ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

add loopback client metadata for oauth in local dev!!!!!

authored by byarielm.fyi and committed by byarielm.fyi d3eacace eade55a5

verified
+37 -23
netlify/functions/batch-follow-users.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 67 67 }; 68 68 } 69 69 70 - // Initialize OAuth client 71 70 const config = getOAuthConfig(); 72 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 73 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 71 + const isDev = config.clientType === 'loopback'; 74 72 75 - const client = new NodeOAuthClient({ 76 - clientMetadata: { 77 - client_id: config.clientId, 78 - client_name: 'ATlast', 79 - client_uri: config.clientId.replace('/client-metadata.json', ''), 80 - redirect_uris: [config.redirectUri], 81 - scope: 'atproto transition:generic', 82 - grant_types: ['authorization_code', 'refresh_token'], 83 - response_types: ['code'], 84 - application_type: 'web', 85 - token_endpoint_auth_method: 'private_key_jwt', 86 - token_endpoint_auth_signing_alg: 'ES256', 87 - dpop_bound_access_tokens: true, 88 - jwks_uri: config.jwksUri, 89 - }, 90 - keyset: [privateKey], 91 - stateStore: stateStore as any, 92 - sessionStore: sessionStore as any, 93 - }); 73 + let client: NodeOAuthClient; 74 + 75 + if (isDev) { 76 + // Loopback 77 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 78 + client = new NodeOAuthClient({ 79 + clientMetadata: clientMetadata, 80 + stateStore: stateStore as any, 81 + sessionStore: sessionStore as any, 82 + }); 83 + } else { 84 + // Production with private key 85 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 86 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 87 + 88 + client = new NodeOAuthClient({ 89 + clientMetadata: { 90 + client_id: config.clientId, 91 + client_name: 'ATlast', 92 + client_uri: config.clientId.replace('/client-metadata.json', ''), 93 + redirect_uris: [config.redirectUri], 94 + scope: 'atproto transition:generic', 95 + grant_types: ['authorization_code', 'refresh_token'], 96 + response_types: ['code'], 97 + application_type: 'web', 98 + token_endpoint_auth_method: 'private_key_jwt', 99 + token_endpoint_auth_signing_alg: 'ES256', 100 + dpop_bound_access_tokens: true, 101 + jwks_uri: config.jwksUri, 102 + }, 103 + keyset: [privateKey], 104 + stateStore: stateStore as any, 105 + sessionStore: sessionStore as any, 106 + }); 107 + } 94 108 95 109 // Restore OAuth session 96 110 const oauthSession = await client.restore(userSession.did);
+37 -23
netlify/functions/batch-search-actors.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 58 58 }; 59 59 } 60 60 61 - // Initialize OAuth client 62 61 const config = getOAuthConfig(); 63 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 64 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 62 + const isDev = config.clientType === 'loopback'; 65 63 66 - const client = new NodeOAuthClient({ 67 - clientMetadata: { 68 - client_id: config.clientId, 69 - client_name: 'ATlast', 70 - client_uri: config.clientId.replace('/client-metadata.json', ''), 71 - redirect_uris: [config.redirectUri], 72 - scope: 'atproto transition:generic', 73 - grant_types: ['authorization_code', 'refresh_token'], 74 - response_types: ['code'], 75 - application_type: 'web', 76 - token_endpoint_auth_method: 'private_key_jwt', 77 - token_endpoint_auth_signing_alg: 'ES256', 78 - dpop_bound_access_tokens: true, 79 - jwks_uri: config.jwksUri, 80 - }, 81 - keyset: [privateKey], 82 - stateStore: stateStore as any, 83 - sessionStore: sessionStore as any, 84 - }); 64 + let client: NodeOAuthClient; 65 + 66 + if (isDev) { 67 + // Loopback 68 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 69 + client = new NodeOAuthClient({ 70 + clientMetadata: clientMetadata, 71 + stateStore: stateStore as any, 72 + sessionStore: sessionStore as any, 73 + }); 74 + } else { 75 + // Production with private key 76 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 77 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 78 + 79 + client = new NodeOAuthClient({ 80 + clientMetadata: { 81 + client_id: config.clientId, 82 + client_name: 'ATlast', 83 + client_uri: config.clientId.replace('/client-metadata.json', ''), 84 + redirect_uris: [config.redirectUri], 85 + scope: 'atproto transition:generic', 86 + grant_types: ['authorization_code', 'refresh_token'], 87 + response_types: ['code'], 88 + application_type: 'web', 89 + token_endpoint_auth_method: 'private_key_jwt', 90 + token_endpoint_auth_signing_alg: 'ES256', 91 + dpop_bound_access_tokens: true, 92 + jwks_uri: config.jwksUri, 93 + }, 94 + keyset: [privateKey], 95 + stateStore: stateStore as any, 96 + sessionStore: sessionStore as any, 97 + }); 98 + } 85 99 86 100 // Restore OAuth session 87 101 const oauthSession = await client.restore(userSession.did);
+36 -12
netlify/functions/client-metadata.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 2 3 3 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 4 + 4 5 try { 5 6 // Get the host that's requesting the metadata 6 7 // This will be different for production vs preview deploys vs dev --live ··· 16 17 }; 17 18 } 18 19 19 - // Build the redirect URI based on the requesting host 20 + // Check if this is a loopback/development request 21 + const isLoopback = requestHost.startsWith('127.0.0.1') || 22 + requestHost.startsWith('[::1]') || 23 + requestHost === 'localhost'; 24 + 25 + if (isLoopback) { 26 + // For loopback clients, return minimal metadata 27 + // NOTE: In practice, the OAuth server won't fetch this because 28 + // loopback clients use hardcoded metadata on the server side 29 + const appUrl = `http://${requestHost}`; 30 + const redirectUri = `${appUrl}/.netlify/functions/oauth-callback`; 31 + 32 + return { 33 + statusCode: 200, 34 + headers: { 35 + 'Content-Type': 'application/json', 36 + 'Access-Control-Allow-Origin': '*', 37 + }, 38 + body: JSON.stringify({ 39 + client_id: appUrl, // Just the origin for loopback 40 + client_name: 'ATlast (Local Dev)', 41 + client_uri: appUrl, 42 + redirect_uris: [redirectUri], 43 + scope: 'atproto transition:generic', 44 + grant_types: ['authorization_code', 'refresh_token'], 45 + response_types: ['code'], 46 + application_type: 'web', 47 + token_endpoint_auth_method: 'none', // No auth for loopback 48 + dpop_bound_access_tokens: true, 49 + }), 50 + }; 51 + } 52 + 53 + // Production: Confidential client metadata 20 54 const redirectUri = `https://${requestHost}/.netlify/functions/oauth-callback`; 21 55 const appUrl = `https://${requestHost}`; 22 56 const jwksUri = `https://${requestHost}/.netlify/functions/jwks`; 23 57 const clientId = `https://${requestHost}/.netlify/functions/client-metadata`; 24 - 25 - console.log('Client metadata generated for host:', { 26 - requestHost, 27 - redirectUri, 28 - appUrl, 29 - clientId, 30 - jwksUri, 31 - }); 32 58 33 59 const metadata = { 34 60 client_id: clientId, ··· 50 76 headers: { 51 77 'Content-Type': 'application/json', 52 78 'Access-Control-Allow-Origin': '*', 53 - 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', 54 - 'Pragma': 'no-cache', 55 - 'Expires': '0', 79 + 'Cache-Control': 'no-store' 56 80 }, 57 81 body: JSON.stringify(metadata), 58 82 };
+70 -58
netlify/functions/oauth-callback.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 13 13 } 14 14 15 15 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 16 - let currentHost = process.env.DEPLOY_URL 17 - ? new URL(process.env.DEPLOY_URL).host 18 - : (event.headers['x-forwarded-host'] || event.headers.host); 19 - let currentUrl = currentHost ? `https://${currentHost}` : process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://atlast.byarielm.fyi'; 20 - const fallbackUrl = currentUrl; 16 + const config = getOAuthConfig(); 17 + const isDev = config.clientType === 'loopback'; 18 + 19 + let currentUrl = isDev 20 + ? 'http://127.0.0.1:8888' 21 + : (process.env.DEPLOY_URL 22 + ? `https://${new URL(process.env.DEPLOY_URL).host}` 23 + : process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://atlast.byarielm.fyi'); 21 24 22 25 try { 23 26 const params = new URLSearchParams(event.rawUrl.split('?')[1] || ''); 24 27 const code = params.get('code'); 25 28 const state = params.get('state'); 26 29 27 - console.log('OAuth callback - Host:', currentHost); 28 - console.log('OAuth callback - currentUrl resolved to:', currentUrl); 30 + console.log('OAuth callback - Mode:', isDev ? 'loopback' : 'production'); 31 + console.log('OAuth callback - URL:', currentUrl); 29 32 30 33 if (!code || !state) { 31 34 return { ··· 37 40 }; 38 41 } 39 42 40 - if (!process.env.OAUTH_PRIVATE_KEY) { 41 - console.error('OAUTH_PRIVATE_KEY not set'); 42 - return { 43 - statusCode: 302, 44 - headers: { 45 - 'Location': `${currentUrl}/?error=Server configuration error` 46 - }, 47 - body: '' 48 - }; 49 - } 50 - 51 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 52 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 53 - 54 - // All URIs must now be based on the current deploy URL/host 55 - const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 56 - const jwksUri = `${currentUrl}/.netlify/functions/jwks`; // NOW DYNAMIC URL 57 - const clientId = `${currentUrl}/.netlify/functions/client-metadata`; // NOW DYNAMIC URL 43 + let client: NodeOAuthClient; 58 44 59 - console.log('OAuth callback URLs:', { 60 - redirectUri, 61 - jwksUri, 62 - clientId, 63 - currentUrl 64 - }); 45 + if (isDev) { 46 + // LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset 47 + console.log('🔧 Loopback callback'); 48 + 49 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 50 + 51 + client = new NodeOAuthClient({ 52 + clientMetadata: clientMetadata, 53 + // No keyset for loopback! 54 + stateStore: stateStore as any, 55 + sessionStore: sessionStore as any, 56 + }); 57 + } else { 58 + // PRODUCTION MODE 59 + if (!process.env.OAUTH_PRIVATE_KEY) { 60 + console.error('OAUTH_PRIVATE_KEY not set'); 61 + return { 62 + statusCode: 302, 63 + headers: { 'Location': `${currentUrl}/?error=Server configuration error` }, 64 + body: '' 65 + }; 66 + } 65 67 66 - // Build metadata dynamically based on the current environment 67 - const clientMetadata = { 68 - client_id: clientId, // NOW DYNAMIC URL 69 - client_name: 'ATlast', 70 - client_uri: currentUrl, 71 - redirect_uris: [redirectUri], 72 - scope: 'atproto transition:generic', 73 - grant_types: ['authorization_code', 'refresh_token'], 74 - response_types: ['code'], 75 - application_type: 'web', 76 - token_endpoint_auth_method: 'private_key_jwt', 77 - token_endpoint_auth_signing_alg: 'ES256', 78 - dpop_bound_access_tokens: true, 79 - jwks_uri: jwksUri, // NOW DYNAMIC URL 80 - }; 68 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 69 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 70 + 71 + const currentHost = process.env.DEPLOY_URL 72 + ? new URL(process.env.DEPLOY_URL).host 73 + : (event.headers['x-forwarded-host'] || event.headers.host); 74 + 75 + currentUrl = `https://${currentHost}`; 76 + const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 77 + const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 78 + const clientId = `${currentUrl}/.netlify/functions/client-metadata`; 81 79 82 - // Initialize OAuth client with dynamic metadata 83 - const client = new NodeOAuthClient({ 84 - clientMetadata: clientMetadata as any, 85 - keyset: [privateKey], 86 - stateStore: stateStore as any, 87 - sessionStore: sessionStore as any, 88 - }); 80 + client = new NodeOAuthClient({ 81 + clientMetadata: { 82 + client_id: clientId, 83 + client_name: 'ATlast', 84 + client_uri: currentUrl, 85 + redirect_uris: [redirectUri], 86 + scope: 'atproto transition:generic', 87 + grant_types: ['authorization_code', 'refresh_token'], 88 + response_types: ['code'], 89 + application_type: 'web', 90 + token_endpoint_auth_method: 'private_key_jwt', 91 + token_endpoint_auth_signing_alg: 'ES256', 92 + dpop_bound_access_tokens: true, 93 + jwks_uri: jwksUri, 94 + } as any, 95 + keyset: [privateKey], 96 + stateStore: stateStore as any, 97 + sessionStore: sessionStore as any, 98 + }); 99 + } 89 100 90 101 const result = await client.callback(params); 91 102 92 - // Store a simple session mapping: sessionId -> DID 103 + // Store session 93 104 const sessionId = crypto.randomUUID(); 94 105 const did = result.session.did; 95 - 96 106 await userSessions.set(sessionId, { did }); 97 107 98 - const cookieFlags = 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/; Secure'; 108 + // Cookie flags - no Secure flag for loopback 109 + const cookieFlags = isDev 110 + ? 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/' 111 + : 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/; Secure'; 99 112 100 113 return { 101 114 statusCode: 302, ··· 108 121 109 122 } catch (error) { 110 123 console.error('OAuth callback error:', error); 111 - 112 124 return { 113 125 statusCode: 302, 114 126 headers: {
+23 -5
netlify/functions/oauth-config.ts
··· 1 1 export function getOAuthConfig() { 2 - // In Netlify, process.env.URL is automatically set to the public URL. 2 + // Development: loopback client for local dev 3 + const isDev = process.env.NODE_ENV === 'development' || process.env.NETLIFY_DEV === 'true'; 4 + 5 + if (isDev) { 6 + const port = process.env.PORT || '8888'; 7 + 8 + // Special loopback client_id format with query params 9 + const clientId = `http://localhost?${new URLSearchParams([ 10 + ['redirect_uri', `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`], 11 + ['scope', 'atproto transition:generic'], 12 + ])}`; 13 + 14 + return { 15 + clientId: clientId, 16 + redirectUri: `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`, 17 + jwksUri: undefined, 18 + clientType: 'loopback' as const, 19 + }; 20 + } 21 + 22 + // Production: discoverable client logic 3 23 const baseUrl = process.env.URL || process.env.DEPLOY_PRIME_URL; 4 24 5 25 if (process.env.NETLIFY && !process.env.URL) { 6 - // This is a safety check for a critical configuration issue on Netlify 7 26 throw new Error('process.env.URL is required in Netlify environment'); 8 27 } 9 28 ··· 13 32 CONTEXT: process.env.CONTEXT, 14 33 using: baseUrl 15 34 }); 16 - 17 - const redirectUri = `${baseUrl}/.netlify/functions/oauth-callback`; 18 35 19 36 return { 20 37 clientId: `${baseUrl}/.netlify/functions/client-metadata`, // discoverable client URL 21 - redirectUri, 38 + redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`, 22 39 jwksUri: `${baseUrl}/.netlify/functions/jwks`, 23 40 clientType: 'discoverable' as const, 41 + usePrivateKey: true, 24 42 }; 25 43 }
+61 -77
netlify/functions/oauth-start.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; 6 - import { initDB } from './db'; // initDB is only kept for manual setup/migrations 7 6 8 7 interface OAuthStartRequestBody { 9 8 login_hint?: string; 10 - origin?: string; // The actual origin the frontend is running on 9 + origin?: string; 11 10 } 12 11 13 12 function normalizePrivateKey(key: string): string { ··· 19 18 20 19 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 21 20 try { 22 - // await initDB(); 23 - 24 - // Parse request body 25 21 let loginHint: string | undefined = undefined; 26 - let requestOrigin: string | undefined = undefined; 27 22 28 23 if (event.body) { 29 24 const parsed: OAuthStartRequestBody = JSON.parse(event.body); 30 25 loginHint = parsed.login_hint; 31 - requestOrigin = parsed.origin; 32 26 } 33 27 34 - // Validate login hint is provided 35 28 if (!loginHint) { 36 29 return { 37 30 statusCode: 400, ··· 39 32 body: JSON.stringify({ error: 'login_hint (handle or DID) is required' }), 40 33 }; 41 34 } 42 - 43 - console.log('OAuth Start - Request origin:', requestOrigin); 44 35 45 - // Validate private key 46 - if (!process.env.OAUTH_PRIVATE_KEY) { 47 - console.error('OAUTH_PRIVATE_KEY not set'); 48 - return { 49 - statusCode: 500, 50 - headers: { 'Content-Type': 'application/json' }, 51 - body: JSON.stringify({ error: 'Server configuration error' }), 52 - }; 53 - } 36 + const config = getOAuthConfig(); 37 + const isDev = config.clientType === 'loopback'; 54 38 55 - // Initialize OAuth client 56 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 57 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 39 + let client: NodeOAuthClient; 58 40 59 - // Use the dynamic config that Netlify sets for the build 60 - const currentHost = process.env.DEPLOY_URL 61 - ? new URL(process.env.DEPLOY_URL).host 62 - : (event.headers['x-forwarded-host'] || event.headers.host); 63 - 64 - if (!currentHost) { 65 - console.error('Missing host header in function request'); 66 - return { 67 - statusCode: 500, 68 - headers: { 'Content-Type': 'application/json' }, 69 - body: JSON.stringify({ error: 'Server could not determine current host for redirect' }), 70 - }; 71 - } 72 - 73 - const currentUrl = `https://${currentHost}`; 41 + if (isDev) { 42 + // LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset 43 + console.log('🔧 Using loopback OAuth client for development'); 44 + console.log('Client ID:', config.clientId); 45 + 46 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 47 + 48 + client = new NodeOAuthClient({ 49 + clientMetadata: clientMetadata, 50 + stateStore: stateStore as any, 51 + sessionStore: sessionStore as any, 52 + }); 53 + } else { 54 + // PRODUCTION MODE: Full confidential client with keyset 55 + console.log('🔐 Using confidential OAuth client for production'); 56 + 57 + if (!process.env.OAUTH_PRIVATE_KEY) { 58 + throw new Error('OAUTH_PRIVATE_KEY required for production'); 59 + } 74 60 75 - // Now, dynamically define the URIs using the CURRENT HOST 76 - const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 77 - const appUrl = currentUrl; 78 - const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 79 - const clientId = `${currentUrl}/.netlify/functions/client-metadata`; 80 - 81 - console.log('OAuth URLs:', { 82 - redirectUri, 83 - appUrl, 84 - jwksUri, 85 - clientId, 86 - requestOrigin 87 - }); 61 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 62 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 88 63 89 - // Build metadata dynamically from environment 90 - const clientMetadata = { 91 - client_id: clientId, 92 - client_name: 'ATlast', 93 - client_uri: appUrl, 94 - redirect_uris: [redirectUri], 95 - scope: 'atproto transition:generic', 96 - grant_types: ['authorization_code', 'refresh_token'], 97 - response_types: ['code'], 98 - application_type: 'web', 99 - token_endpoint_auth_method: 'private_key_jwt', 100 - token_endpoint_auth_signing_alg: 'ES256', 101 - dpop_bound_access_tokens: true, 102 - jwks_uri: jwksUri, 103 - }; 64 + const currentHost = process.env.DEPLOY_URL 65 + ? new URL(process.env.DEPLOY_URL).host 66 + : (event.headers['x-forwarded-host'] || event.headers.host); 104 67 105 - console.log('Client metadata:', clientMetadata); 68 + if (!currentHost) { 69 + throw new Error('Missing host header'); 70 + } 106 71 107 - // Initialize NodeOAuthClient with typed stores 108 - const client = new NodeOAuthClient({ 109 - clientMetadata: clientMetadata as any, 110 - keyset: [privateKey], 111 - stateStore: stateStore as any, 112 - sessionStore: sessionStore as any, 113 - }); 72 + const currentUrl = `https://${currentHost}`; 73 + const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 74 + const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 75 + const clientId = `${currentUrl}/.netlify/functions/client-metadata`; 114 76 115 - console.log('OAuth client initialized with redirect_uri:', redirectUri); 77 + client = new NodeOAuthClient({ 78 + clientMetadata: { 79 + client_id: clientId, 80 + client_name: 'ATlast', 81 + client_uri: currentUrl, 82 + redirect_uris: [redirectUri], 83 + scope: 'atproto transition:generic', 84 + grant_types: ['authorization_code', 'refresh_token'], 85 + response_types: ['code'], 86 + application_type: 'web', 87 + token_endpoint_auth_method: 'private_key_jwt', 88 + token_endpoint_auth_signing_alg: 'ES256', 89 + dpop_bound_access_tokens: true, 90 + jwks_uri: jwksUri, 91 + } as any, 92 + keyset: [privateKey], 93 + stateStore: stateStore as any, 94 + sessionStore: sessionStore as any, 95 + }); 96 + } 116 97 117 - // Generate authorization URL 118 98 const authUrl = await client.authorize(loginHint, { 119 99 scope: 'atproto transition:generic', 120 100 }); ··· 129 109 return { 130 110 statusCode: 500, 131 111 headers: { 'Content-Type': 'application/json' }, 132 - body: JSON.stringify({ error: 'Failed to start OAuth flow' }), 112 + body: JSON.stringify({ 113 + error: 'Failed to start OAuth flow', 114 + details: error instanceof Error ? error.message : 'Unknown error', 115 + stack: error instanceof Error ? error.stack : undefined 116 + }), 133 117 }; 134 118 } 135 119 };
+37 -22
netlify/functions/session.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 106 106 // Cache miss - fetch full profile 107 107 try { 108 108 const config = getOAuthConfig(); 109 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 110 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 109 + const isDev = config.clientType === 'loopback'; 110 + 111 + let client: NodeOAuthClient; 112 + 113 + if (isDev) { 114 + // Loopback 115 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 116 + client = new NodeOAuthClient({ 117 + clientMetadata: clientMetadata, 118 + stateStore: stateStore as any, 119 + sessionStore: sessionStore as any, 120 + }); 121 + } else { 122 + // Production with private key 123 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 124 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 111 125 112 - const client = new NodeOAuthClient({ 113 - clientMetadata: { 114 - client_id: config.clientId, 115 - client_name: 'ATlast', 116 - client_uri: config.clientId.replace('/client-metadata.json', ''), 117 - redirect_uris: [config.redirectUri], 118 - scope: 'atproto transition:generic', 119 - grant_types: ['authorization_code', 'refresh_token'], 120 - response_types: ['code'], 121 - application_type: 'web', 122 - token_endpoint_auth_method: 'private_key_jwt', 123 - token_endpoint_auth_signing_alg: 'ES256', 124 - dpop_bound_access_tokens: true, 125 - jwks_uri: config.jwksUri, 126 - }, 127 - keyset: [privateKey], 128 - stateStore: stateStore as any, 129 - sessionStore: sessionStore as any, 130 - }); 126 + client = new NodeOAuthClient({ 127 + clientMetadata: { 128 + client_id: config.clientId, 129 + client_name: 'ATlast', 130 + client_uri: config.clientId.replace('/client-metadata.json', ''), 131 + redirect_uris: [config.redirectUri], 132 + scope: 'atproto transition:generic', 133 + grant_types: ['authorization_code', 'refresh_token'], 134 + response_types: ['code'], 135 + application_type: 'web', 136 + token_endpoint_auth_method: 'private_key_jwt', 137 + token_endpoint_auth_signing_alg: 'ES256', 138 + dpop_bound_access_tokens: true, 139 + jwks_uri: config.jwksUri, 140 + }, 141 + keyset: [privateKey], 142 + stateStore: stateStore as any, 143 + sessionStore: sessionStore as any, 144 + }); 145 + } 131 146 132 147 // Restore OAuth session 133 148 const oauthSession = await client.restore(did);