From aae225d442bb1029471e0ff2609f50273ce02a2b Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 9 Nov 2025 15:08:04 +0000 Subject: [PATCH] feat: Make OAuth configuration environment-aware via NEXT_PUBLIC_APP_URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .example.env | 8 +++--- app/client-metadata.json/route.ts | 34 +++++++++++++++++++++++++ lib/auth/oauth-client.ts | 41 +++++++++++++++++++++---------- public/client-metadata.json | 20 --------------- 4 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 app/client-metadata.json/route.ts delete mode 100644 public/client-metadata.json diff --git a/.example.env b/.example.env index d6990df..ff6d714 100644 --- a/.example.env +++ b/.example.env @@ -15,10 +15,10 @@ GOOGLE_AI_MODEL=gemini-pro-latest # Deepgram API Key (for voice-to-text) DEEPGRAM_API_KEY=your-deepgram-api-key -# Bluesky/ATproto OAuth Configuration (localhost development mode) -# See: https://atproto.com/specs/oauth#localhost-client-development -BLUESKY_CLIENT_ID=http://localhost/?redirect_uri=http://127.0.0.1:3000/api/auth/callback -BLUESKY_REDIRECT_URI=http://127.0.0.1:3000/api/auth/callback +# Application URL (used for OAuth callbacks and client metadata) +# In development, defaults to http://localhost:3000 +# In production, set to your domain (e.g., https://www.ponderants.com) +NEXT_PUBLIC_APP_URL=http://localhost:3000 # Test Account Credentials (for E2E tests) TEST_BLUESKY_HANDLE=your-test-bluesky-handle diff --git a/app/client-metadata.json/route.ts b/app/client-metadata.json/route.ts new file mode 100644 index 0000000..4873aba --- /dev/null +++ b/app/client-metadata.json/route.ts @@ -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 + }, + }); +} diff --git a/lib/auth/oauth-client.ts b/lib/auth/oauth-client.ts index a5e9501..1b840ce 100644 --- a/lib/auth/oauth-client.ts +++ b/lib/auth/oauth-client.ts @@ -15,7 +15,7 @@ let clientInstance: NodeOAuthClient | null = null; * Get or create the singleton OAuth client instance. * * 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: * - OAuth authorization flow with PKCE @@ -29,11 +29,8 @@ export async function getOAuthClient(): Promise { } const isDev = process.env.NODE_ENV === 'development'; - const callbackUrl = process.env.BLUESKY_REDIRECT_URI; - - if (!callbackUrl) { - throw new Error('BLUESKY_REDIRECT_URI environment variable is required'); - } + const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const callbackUrl = `${appUrl}/api/auth/callback`; if (isDev) { // Development: Use localhost loopback client @@ -64,13 +61,31 @@ export async function getOAuthClient(): Promise { console.log('[OAuth] ✓ Development client initialized'); } else { - // Production: Backend service with keys - // TODO: Implement when deploying to production - // See plans/oauth-dpop-implementation.md for details - throw new Error( - 'Production OAuth client not yet implemented. ' + - 'See plans/oauth-dpop-implementation.md for production setup instructions.' - ); + // Production: Use client metadata from /client-metadata.json endpoint + const clientId = `${appUrl}/client-metadata.json`; + + console.log('[OAuth] Initializing production client'); + console.log('[OAuth] client_id:', clientId); + 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; diff --git a/public/client-metadata.json b/public/client-metadata.json deleted file mode 100644 index b8559ab..0000000 --- a/public/client-metadata.json +++ /dev/null @@ -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 -}