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=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
|
||||
|
||||
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.
|
||||
*
|
||||
* 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<NodeOAuthClient> {
|
||||
}
|
||||
|
||||
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<NodeOAuthClient> {
|
||||
|
||||
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;
|
||||
|
||||
@@ -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