forked from
atpota.to/flushes.app
The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
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}