feat: Make OAuth configuration environment-aware via NEXT_PUBLIC_APP_URL
- Convert client-metadata.json to dynamic API route reading from env vars - Remove BLUESKY_CLIENT_ID and BLUESKY_REDIRECT_URI env vars - All OAuth URLs now derived from NEXT_PUBLIC_APP_URL - Implement production OAuth client (removes TODO/placeholder) - Update .prod.env with production settings for www.ponderants.com - Use https:// for production URLs - Simplify environment configuration (single source of truth) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,10 +15,10 @@ GOOGLE_AI_MODEL=gemini-pro-latest
|
|||||||
# Deepgram API Key (for voice-to-text)
|
# Deepgram API Key (for voice-to-text)
|
||||||
DEEPGRAM_API_KEY=your-deepgram-api-key
|
DEEPGRAM_API_KEY=your-deepgram-api-key
|
||||||
|
|
||||||
# Bluesky/ATproto OAuth Configuration (localhost development mode)
|
# Application URL (used for OAuth callbacks and client metadata)
|
||||||
# See: https://atproto.com/specs/oauth#localhost-client-development
|
# In development, defaults to http://localhost:3000
|
||||||
BLUESKY_CLIENT_ID=http://localhost/?redirect_uri=http://127.0.0.1:3000/api/auth/callback
|
# In production, set to your domain (e.g., https://www.ponderants.com)
|
||||||
BLUESKY_REDIRECT_URI=http://127.0.0.1:3000/api/auth/callback
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
|
||||||
# Test Account Credentials (for E2E tests)
|
# Test Account Credentials (for E2E tests)
|
||||||
TEST_BLUESKY_HANDLE=your-test-bluesky-handle
|
TEST_BLUESKY_HANDLE=your-test-bluesky-handle
|
||||||
|
|||||||
34
app/client-metadata.json/route.ts
Normal file
34
app/client-metadata.json/route.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ATproto OAuth Client Metadata Endpoint
|
||||||
|
*
|
||||||
|
* This endpoint serves the OAuth client metadata required for ATproto authentication.
|
||||||
|
* The client_id must match the URL where this metadata is served.
|
||||||
|
*
|
||||||
|
* @see https://atproto.com/specs/oauth
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
client_id: `${appUrl}/client-metadata.json`,
|
||||||
|
client_name: 'Ponderants',
|
||||||
|
client_uri: appUrl,
|
||||||
|
logo_uri: `${appUrl}/logo.svg`,
|
||||||
|
redirect_uris: [`${appUrl}/api/auth/callback`],
|
||||||
|
scope: 'atproto transition:generic',
|
||||||
|
grant_types: ['authorization_code', 'refresh_token'],
|
||||||
|
response_types: ['code'],
|
||||||
|
token_endpoint_auth_method: 'none',
|
||||||
|
application_type: 'web',
|
||||||
|
dpop_bound_access_tokens: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(metadata, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ let clientInstance: NodeOAuthClient | null = null;
|
|||||||
* Get or create the singleton OAuth client instance.
|
* Get or create the singleton OAuth client instance.
|
||||||
*
|
*
|
||||||
* In development, uses the localhost client exception (no keys needed).
|
* In development, uses the localhost client exception (no keys needed).
|
||||||
* In production, uses backend service with private keys (TODO).
|
* In production, uses client metadata served from /client-metadata.json.
|
||||||
*
|
*
|
||||||
* The client handles:
|
* The client handles:
|
||||||
* - OAuth authorization flow with PKCE
|
* - OAuth authorization flow with PKCE
|
||||||
@@ -29,11 +29,8 @@ export async function getOAuthClient(): Promise<NodeOAuthClient> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
const callbackUrl = process.env.BLUESKY_REDIRECT_URI;
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
||||||
|
const callbackUrl = `${appUrl}/api/auth/callback`;
|
||||||
if (!callbackUrl) {
|
|
||||||
throw new Error('BLUESKY_REDIRECT_URI environment variable is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// Development: Use localhost loopback client
|
// Development: Use localhost loopback client
|
||||||
@@ -64,13 +61,31 @@ export async function getOAuthClient(): Promise<NodeOAuthClient> {
|
|||||||
|
|
||||||
console.log('[OAuth] ✓ Development client initialized');
|
console.log('[OAuth] ✓ Development client initialized');
|
||||||
} else {
|
} else {
|
||||||
// Production: Backend service with keys
|
// Production: Use client metadata from /client-metadata.json endpoint
|
||||||
// TODO: Implement when deploying to production
|
const clientId = `${appUrl}/client-metadata.json`;
|
||||||
// See plans/oauth-dpop-implementation.md for details
|
|
||||||
throw new Error(
|
console.log('[OAuth] Initializing production client');
|
||||||
'Production OAuth client not yet implemented. ' +
|
console.log('[OAuth] client_id:', clientId);
|
||||||
'See plans/oauth-dpop-implementation.md for production setup instructions.'
|
console.log('[OAuth] callback_url:', callbackUrl);
|
||||||
);
|
|
||||||
|
clientInstance = new NodeOAuthClient({
|
||||||
|
clientMetadata: {
|
||||||
|
client_id: clientId,
|
||||||
|
client_name: 'Ponderants',
|
||||||
|
client_uri: appUrl,
|
||||||
|
redirect_uris: [callbackUrl],
|
||||||
|
scope: 'atproto transition:generic',
|
||||||
|
grant_types: ['authorization_code', 'refresh_token'],
|
||||||
|
response_types: ['code'],
|
||||||
|
token_endpoint_auth_method: 'none',
|
||||||
|
application_type: 'web',
|
||||||
|
dpop_bound_access_tokens: true,
|
||||||
|
},
|
||||||
|
stateStore: createStateStore(),
|
||||||
|
sessionStore: createSessionStore(),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[OAuth] ✓ Production client initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
return clientInstance;
|
return clientInstance;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"client_id": "https://www.ponderants.com/client-metadata.json",
|
|
||||||
"client_name": "Ponderants",
|
|
||||||
"client_uri": "https://www.ponderants.com",
|
|
||||||
"logo_uri": "https://www.ponderants.com/logo.svg",
|
|
||||||
"redirect_uris": [
|
|
||||||
"https://www.ponderants.com/api/auth/callback"
|
|
||||||
],
|
|
||||||
"scope": "atproto transition:generic",
|
|
||||||
"grant_types": [
|
|
||||||
"authorization_code",
|
|
||||||
"refresh_token"
|
|
||||||
],
|
|
||||||
"response_types": [
|
|
||||||
"code"
|
|
||||||
],
|
|
||||||
"token_endpoint_auth_method": "none",
|
|
||||||
"application_type": "web",
|
|
||||||
"dpop_bound_access_tokens": true
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user