The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
at main 205 lines 7.6 kB view raw
1import { NextRequest, NextResponse } from 'next/server'; 2 3// Configure this route as dynamic to fix static generation issues 4export const dynamic = 'force-dynamic'; 5 6const DEFAULT_AUTH_SERVER = 'https://public.api.bsky.app'; 7const REDIRECT_URI = 'https://flushes.app/auth/callback'; 8const CLIENT_ID = 'https://flushes.app/oauth-client-metadata.json'; 9 10// Function to get a nonce from the specified PDS 11async function getNonce(pdsEndpoint: string): Promise<string | null> { 12 try { 13 const tokenEndpoint = `${pdsEndpoint}/oauth/token`; 14 const headResponse = await fetch(tokenEndpoint, { 15 method: 'HEAD', 16 headers: { 17 'Accept': '*/*' 18 } 19 }); 20 21 return headResponse.headers.get('DPoP-Nonce'); 22 } catch (error) { 23 console.error('Error getting nonce:', error); 24 return null; 25 } 26} 27 28export async function POST(request: NextRequest) { 29 try { 30 // Parse the request body 31 const body = await request.json(); 32 const { code, codeVerifier, dpopToken, pdsEndpoint, originalPdsEndpoint } = body; 33 34 // Enhanced logging 35 console.log('[TOKEN ROUTE] Request parameters:', { 36 code: code ? code.substring(0, 6) + '...' : 'none', // Only log first few chars of sensitive data 37 codeVerifier: codeVerifier ? codeVerifier.substring(0, 6) + '...' : 'none', 38 pdsEndpoint, 39 originalPdsEndpoint, 40 dpopTokenProvided: !!dpopToken 41 }); 42 43 // CRITICAL FIX: Use the correct token endpoint based on PDS type 44 // - For bsky.network PDSes: always use public.api.bsky.app for token exchange 45 // - For public.api.bsky.app: use it directly 46 // - For third-party PDSes: use their own endpoint 47 let authServer = pdsEndpoint; 48 49 // If it's a bsky.network PDS, use public.api.bsky.app 50 if (pdsEndpoint.includes('bsky.network')) { 51 console.log(`[TOKEN ROUTE] Using public.api.bsky.app for bsky.network PDS: ${pdsEndpoint}`); 52 authServer = DEFAULT_AUTH_SERVER; 53 } else if (pdsEndpoint.includes('public.api.bsky.app')) { 54 // Already using public.api.bsky.app 55 console.log(`[TOKEN ROUTE] Using public.api.bsky.app endpoint directly`); 56 } else { 57 console.log(`[TOKEN ROUTE] Using third-party PDS's own endpoint for token exchange: ${pdsEndpoint}`); 58 } 59 60 // Default to public.api.bsky.app if no PDS endpoint provided 61 if (!pdsEndpoint) { 62 console.log(`[TOKEN ROUTE] No PDS endpoint provided, using default: ${DEFAULT_AUTH_SERVER}`); 63 authServer = DEFAULT_AUTH_SERVER; 64 } 65 66 if (!code || !codeVerifier || !dpopToken) { 67 const missingParams = []; 68 if (!code) missingParams.push('code'); 69 if (!codeVerifier) missingParams.push('codeVerifier'); 70 if (!dpopToken) missingParams.push('dpopToken'); 71 72 console.error(`[TOKEN ROUTE] Missing required parameters: ${missingParams.join(', ')}`); 73 return NextResponse.json( 74 { error: 'Missing required parameters', missing: missingParams }, 75 { status: 400 } 76 ); 77 } 78 79 // Get a nonce from the specified PDS 80 const nonce = await getNonce(authServer); 81 console.log(`[TOKEN ROUTE] Got nonce from server-side (${authServer}):`, nonce); 82 83 // Forward the token request to the specified PDS 84 const tokenEndpoint = `${authServer}/oauth/token`; 85 console.log(`[TOKEN ROUTE] Making token request to: ${tokenEndpoint}`); 86 87 // Prepare the form data 88 const formData = new URLSearchParams({ 89 grant_type: 'authorization_code', 90 code, 91 redirect_uri: REDIRECT_URI, 92 client_id: CLIENT_ID, 93 code_verifier: codeVerifier 94 }); 95 96 // CRITICAL FIX: We only need to add cross-domain parameters when using public.api.bsky.app 97 // But we'll keep this logic in case it's needed for specific PDS implementations 98 if (originalPdsEndpoint && originalPdsEndpoint !== authServer) { 99 console.log(`[TOKEN ROUTE] Cross-domain token exchange detected`); 100 console.log(`[TOKEN ROUTE] Not adding cross-domain parameters as we're using direct PDS endpoints`); 101 } 102 103 // Log the complete request for debugging 104 console.log('[TOKEN ROUTE] Complete token request:', { 105 url: tokenEndpoint, 106 method: 'POST', 107 headers: { 108 'Content-Type': 'application/x-www-form-urlencoded', 109 'DPoP': dpopToken ? '[TOKEN PRESENT]' : '[MISSING]' 110 }, 111 formData: Object.fromEntries(formData) 112 }); 113 114 const response = await fetch(tokenEndpoint, { 115 method: 'POST', 116 headers: { 117 'Content-Type': 'application/x-www-form-urlencoded', 118 'DPoP': dpopToken, 119 // Include any additional headers needed 120 }, 121 body: formData 122 }); 123 124 // Log response status and headers 125 console.log(`[TOKEN ROUTE] Response status: ${response.status}`); 126 127 // Log headers in a TypeScript-compatible way 128 const headers: Record<string, string> = {}; 129 response.headers.forEach((value, key) => { 130 headers[key] = value; 131 }); 132 console.log('[TOKEN ROUTE] Response headers:', headers); 133 134 // Get the response data 135 const responseData = await response.json(); 136 137 // Log complete error response for debugging 138 if (!response.ok) { 139 console.error('[TOKEN ROUTE] Token request failed with status:', response.status); 140 console.error('[TOKEN ROUTE] Error response:', responseData); 141 142 // For invalid_grant errors, provide more context 143 if (responseData.error === 'invalid_grant') { 144 console.error(`[TOKEN ROUTE] Invalid grant error details: 145 - The authorization code might have expired 146 - The code_verifier might not match what was used for code_challenge 147 - For third-party PDS: resource parameter might be incorrect 148 - Client ID might not match what was used in authorization request 149 - Redirect URI might not match what was used in authorization request 150 `); 151 } 152 } 153 154 // If there's an error about missing nonce, return the nonce 155 if (responseData.error === 'use_dpop_nonce') { 156 const dpopNonce = response.headers.get('DPoP-Nonce'); 157 console.log(`[TOKEN ROUTE] Got DPoP-Nonce from error response: ${dpopNonce}`); 158 return NextResponse.json( 159 { 160 error: 'use_dpop_nonce', 161 nonce: dpopNonce, 162 originalError: responseData 163 }, 164 { status: 400 } 165 ); 166 } 167 168 // Log the token response for debugging (with sensitive data redacted) 169 if (response.ok) { 170 console.log('[TOKEN ROUTE] Token response from Bluesky:', JSON.stringify({ 171 ...responseData, 172 access_token: responseData.access_token ? '[REDACTED]' : null, 173 refresh_token: responseData.refresh_token ? '[REDACTED]' : null, 174 })); 175 176 // Check if we have an audience in the token 177 if (responseData.aud) { 178 console.log('[TOKEN ROUTE] Token audience:', responseData.aud); 179 } else { 180 console.warn('[TOKEN ROUTE] No audience in token response'); 181 } 182 } 183 184 // Return the response 185 return NextResponse.json(responseData, { status: response.status }); 186 } catch (error: any) { 187 console.error('Token proxy error:', error); 188 return NextResponse.json( 189 { error: 'Token proxy error', message: error.message }, 190 { status: 500 } 191 ); 192 } 193} 194 195// Handle OPTIONS requests for CORS 196export async function OPTIONS() { 197 return new NextResponse(null, { 198 status: 200, 199 headers: { 200 'Access-Control-Allow-Origin': '*', 201 'Access-Control-Allow-Methods': 'POST, OPTIONS', 202 'Access-Control-Allow-Headers': 'Content-Type, DPoP', 203 }, 204 }); 205}