···
7
7
"tos_uri": "https://flushes.app/terms",
8
8
"policy_uri": "https://flushes.app/privacy",
9
9
"dpop_bound_access_tokens": true,
10
10
+
"authorization_endpoint": "https://bsky.social/oauth/authorize",
11
11
+
"token_endpoint": "https://bsky.social/oauth/token",
12
12
+
"issuer": "https://bsky.social",
10
13
"grant_types": [
11
14
"authorization_code",
12
15
"refresh_token"
···
31
31
const body = await request.json();
32
32
const { code, codeVerifier, dpopToken, pdsEndpoint, originalPdsEndpoint } = body;
33
33
34
34
+
// Enhanced logging
35
35
+
console.log('[TOKEN ROUTE] Request parameters:', {
36
36
+
code: code ? code.substring(0, 6) + '...' : 'none', // Only log first few chars of sensitive data
37
37
+
codeVerifier: codeVerifier ? codeVerifier.substring(0, 6) + '...' : 'none',
38
38
+
pdsEndpoint,
39
39
+
originalPdsEndpoint,
40
40
+
dpopTokenProvided: !!dpopToken
41
41
+
});
42
42
+
34
43
// Use the provided PDS endpoint or default to Bluesky's
35
44
// CRITICAL FIX: For third-party PDS, always use bsky.social for token requests
36
45
let authServer = pdsEndpoint || DEFAULT_AUTH_SERVER;
37
46
if (pdsEndpoint && !pdsEndpoint.includes('bsky.social')) {
38
38
-
console.log(`Redirecting token request to bsky.social for third-party PDS: ${pdsEndpoint}`);
47
47
+
console.log(`[TOKEN ROUTE] Redirecting token request to bsky.social for third-party PDS: ${pdsEndpoint}`);
39
48
authServer = DEFAULT_AUTH_SERVER;
40
49
}
41
50
42
51
if (!code || !codeVerifier || !dpopToken) {
52
52
+
const missingParams = [];
53
53
+
if (!code) missingParams.push('code');
54
54
+
if (!codeVerifier) missingParams.push('codeVerifier');
55
55
+
if (!dpopToken) missingParams.push('dpopToken');
56
56
+
57
57
+
console.error(`[TOKEN ROUTE] Missing required parameters: ${missingParams.join(', ')}`);
43
58
return NextResponse.json(
44
44
-
{ error: 'Missing required parameters' },
59
59
+
{ error: 'Missing required parameters', missing: missingParams },
45
60
{ status: 400 }
46
61
);
47
62
}
48
63
49
64
// Get a nonce from the specified PDS
50
65
const nonce = await getNonce(authServer);
51
51
-
console.log(`Got nonce from server-side (${authServer}):`, nonce);
66
66
+
console.log(`[TOKEN ROUTE] Got nonce from server-side (${authServer}):`, nonce);
52
67
53
68
// Forward the token request to the specified PDS
54
69
const tokenEndpoint = `${authServer}/oauth/token`;
55
55
-
console.log(`Making token request to: ${tokenEndpoint}`);
70
70
+
console.log(`[TOKEN ROUTE] Making token request to: ${tokenEndpoint}`);
56
71
57
72
// Prepare the form data
58
73
const formData = new URLSearchParams({
···
63
78
code_verifier: codeVerifier
64
79
});
65
80
66
66
-
// For third-party PDS, add the 'resource' parameter with the original PDS endpoint
67
67
-
// This is CRITICAL for the token exchange to work with third-party PDS servers
81
81
+
// For third-party PDS, add the 'resource' AND 'issuer' parameters
82
82
+
// These are CRITICAL for the token exchange to work with third-party PDS servers
68
83
if (originalPdsEndpoint && originalPdsEndpoint !== authServer) {
69
69
-
console.log(`Adding resource parameter for third-party PDS: ${originalPdsEndpoint}`);
84
84
+
console.log(`[TOKEN ROUTE] Adding resource parameter for third-party PDS: ${originalPdsEndpoint}`);
70
85
formData.append('resource', originalPdsEndpoint);
86
86
+
87
87
+
// Add the issuer parameter which is required for cross-domain OAuth
88
88
+
console.log(`[TOKEN ROUTE] Adding issuer parameter for third-party PDS: ${originalPdsEndpoint}`);
89
89
+
formData.append('issuer', originalPdsEndpoint);
71
90
}
72
91
92
92
+
// Log the complete request for debugging
93
93
+
console.log('[TOKEN ROUTE] Complete token request:', {
94
94
+
url: tokenEndpoint,
95
95
+
method: 'POST',
96
96
+
headers: {
97
97
+
'Content-Type': 'application/x-www-form-urlencoded',
98
98
+
'DPoP': dpopToken ? '[TOKEN PRESENT]' : '[MISSING]'
99
99
+
},
100
100
+
formData: Object.fromEntries(formData)
101
101
+
});
102
102
+
73
103
const response = await fetch(tokenEndpoint, {
74
104
method: 'POST',
75
105
headers: {
···
80
110
body: formData
81
111
});
82
112
113
113
+
// Log response status and headers
114
114
+
console.log(`[TOKEN ROUTE] Response status: ${response.status}`);
115
115
+
console.log('[TOKEN ROUTE] Response headers:', Object.fromEntries([...response.headers.entries()]));
116
116
+
83
117
// Get the response data
84
118
const responseData = await response.json();
85
119
120
120
+
// Log complete error response for debugging
121
121
+
if (!response.ok) {
122
122
+
console.error('[TOKEN ROUTE] Token request failed with status:', response.status);
123
123
+
console.error('[TOKEN ROUTE] Error response:', responseData);
124
124
+
125
125
+
// For invalid_grant errors, provide more context
126
126
+
if (responseData.error === 'invalid_grant') {
127
127
+
console.error(`[TOKEN ROUTE] Invalid grant error details:
128
128
+
- The authorization code might have expired
129
129
+
- The code_verifier might not match what was used for code_challenge
130
130
+
- For third-party PDS: resource parameter might be incorrect
131
131
+
- Client ID might not match what was used in authorization request
132
132
+
- Redirect URI might not match what was used in authorization request
133
133
+
`);
134
134
+
}
135
135
+
}
136
136
+
86
137
// If there's an error about missing nonce, return the nonce
87
138
if (responseData.error === 'use_dpop_nonce') {
88
139
const dpopNonce = response.headers.get('DPoP-Nonce');
140
140
+
console.log(`[TOKEN ROUTE] Got DPoP-Nonce from error response: ${dpopNonce}`);
89
141
return NextResponse.json(
90
142
{
91
143
error: 'use_dpop_nonce',
···
96
148
);
97
149
}
98
150
99
99
-
// Log the token response for debugging
100
100
-
console.log('Token response from Bluesky:', JSON.stringify({
101
101
-
...responseData,
102
102
-
access_token: responseData.access_token ? '[REDACTED]' : null,
103
103
-
refresh_token: responseData.refresh_token ? '[REDACTED]' : null,
104
104
-
}));
105
105
-
106
106
-
// Check if we have an audience in the token
107
107
-
if (responseData.aud) {
108
108
-
console.log('Token audience:', responseData.aud);
109
109
-
} else {
110
110
-
console.warn('No audience in token response');
151
151
+
// Log the token response for debugging (with sensitive data redacted)
152
152
+
if (response.ok) {
153
153
+
console.log('[TOKEN ROUTE] Token response from Bluesky:', JSON.stringify({
154
154
+
...responseData,
155
155
+
access_token: responseData.access_token ? '[REDACTED]' : null,
156
156
+
refresh_token: responseData.refresh_token ? '[REDACTED]' : null,
157
157
+
}));
158
158
+
159
159
+
// Check if we have an audience in the token
160
160
+
if (responseData.aud) {
161
161
+
console.log('[TOKEN ROUTE] Token audience:', responseData.aud);
162
162
+
} else {
163
163
+
console.warn('[TOKEN ROUTE] No audience in token response');
164
164
+
}
111
165
}
112
166
113
167
// Return the response
···
48
48
const code = searchParams.get('code');
49
49
const state = searchParams.get('state');
50
50
const iss = searchParams.get('iss');
51
51
+
const error = searchParams.get('error');
52
52
+
const errorDescription = searchParams.get('error_description');
53
53
+
54
54
+
// Log all URL parameters for debugging
55
55
+
console.log('Callback URL parameters:', {
56
56
+
code: code ? code.substring(0, 6) + '...' : null,
57
57
+
state: state ? state.substring(0, 5) + '...' : null,
58
58
+
iss,
59
59
+
error,
60
60
+
errorDescription,
61
61
+
// Log any additional parameters
62
62
+
allParams: Object.fromEntries(searchParams)
63
63
+
});
64
64
+
65
65
+
// Check for error parameters in the callback
66
66
+
if (error) {
67
67
+
console.error(`OAuth error in callback: ${error} - ${errorDescription}`);
68
68
+
setError(`Authentication error: ${errorDescription || error}`);
69
69
+
return;
70
70
+
}
51
71
52
72
if (!code || !state || !iss) {
53
53
-
setError('Invalid callback parameters');
73
73
+
const missing = [];
74
74
+
if (!code) missing.push('code');
75
75
+
if (!state) missing.push('state');
76
76
+
if (!iss) missing.push('iss');
77
77
+
78
78
+
console.error(`Missing required callback parameters: ${missing.join(', ')}`);
79
79
+
setError(`Invalid callback parameters. Missing: ${missing.join(', ')}`);
54
80
return;
55
81
}
56
82
57
83
// Get stored values from our robust storage utility
58
84
if (typeof window === 'undefined') {
85
85
+
console.error('Browser environment not available');
59
86
setError('Browser environment not available');
60
87
return;
61
88
}
···
63
90
const storedState = retrieveAuthData('oauth_state');
64
91
const codeVerifier = retrieveAuthData('code_verifier');
65
92
const serializedKeyPair = retrieveAuthData('key_pair');
93
93
+
94
94
+
// Log stored auth data for debugging
95
95
+
console.log('Stored auth data:', {
96
96
+
storedStateExists: !!storedState,
97
97
+
storedStatePrefix: storedState ? storedState.substring(0, 5) + '...' : null,
98
98
+
codeVerifierExists: !!codeVerifier,
99
99
+
codeVerifierLength: codeVerifier ? codeVerifier.length : 0,
100
100
+
serializedKeyPairExists: !!serializedKeyPair,
101
101
+
storageFunctioning: typeof localStorage !== 'undefined' && typeof sessionStorage !== 'undefined'
102
102
+
});
66
103
67
104
// Check if we have the stored values
68
105
if (!storedState) {
69
69
-
setError('Session data lost. Please try logging in again.');
106
106
+
console.error('Session state data lost. Storage might be disabled or corrupted.');
107
107
+
setError('Session data lost. Please try logging in again, ensuring cookies and local storage are enabled.');
70
108
return;
71
109
}
72
110
73
111
// Validate state
74
112
if (state !== storedState) {
75
75
-
console.error('State mismatch. Received:', state, 'Stored:', storedState);
113
113
+
console.error('State mismatch:', {
114
114
+
received: state ? state.substring(0, 10) + '...' : null,
115
115
+
stored: storedState ? storedState.substring(0, 10) + '...' : null,
116
116
+
match: state === storedState
117
117
+
});
76
118
setError('Invalid state parameter. This may be due to an expired session or a security issue.');
77
119
return;
78
120
}
79
121
122
122
+
// Validate the rest of the auth data
80
123
if (!codeVerifier || !serializedKeyPair) {
81
81
-
setError('Missing authorization data');
124
124
+
const missing = [];
125
125
+
if (!codeVerifier) missing.push('code_verifier');
126
126
+
if (!serializedKeyPair) missing.push('key_pair');
127
127
+
128
128
+
console.error(`Missing authorization data: ${missing.join(', ')}`);
129
129
+
setError(`Missing authorization data: ${missing.join(', ')}. Please try logging in again.`);
82
130
return;
83
131
}
84
132
···
117
165
console.log('Exchanging code for token...');
118
166
let tokenResponse;
119
167
try {
120
120
-
// CRITICAL FIX: For third-party PDS, we need to use bsky.social for token exchange
121
121
-
// But we must pass the original PDS endpoint (iss param) as well
168
168
+
// CRITICAL FIX: For third-party PDS, we need special handling for token exchange
122
169
let authServer = storedAuthServer || 'https://bsky.social';
123
170
let tokenPdsEndpoint = storedPdsEndpoint;
124
171
···
129
176
tokenPdsEndpoint = iss;
130
177
// Store this for later use
131
178
storeAuthData('pds_endpoint', iss);
179
179
+
180
180
+
// For third-party PDS, we need to ensure we're using the right auth server
181
181
+
if (!iss.includes('bsky.social')) {
182
182
+
// Always use bsky.social for token exchange with third-party PDS
183
183
+
authServer = 'https://bsky.social';
184
184
+
console.log('Third-party PDS detected, using bsky.social as auth server');
185
185
+
186
186
+
// Also store the auth server
187
187
+
storeAuthData('auth_server', authServer);
188
188
+
}
132
189
}
133
190
134
134
-
// Always use bsky.social for token exchange (even for custom PDS endpoints)
135
135
-
console.log('Using auth server for token exchange:', authServer);
136
136
-
console.log('Original PDS endpoint (iss):', tokenPdsEndpoint);
191
191
+
console.log('Authentication servers:', {
192
192
+
authServer,
193
193
+
originalPdsEndpoint: tokenPdsEndpoint,
194
194
+
isThirdPartyPds: tokenPdsEndpoint !== authServer
195
195
+
});
137
196
138
197
// Convert null to undefined for type compatibility
139
198
const originalPdsEndpoint = tokenPdsEndpoint === null ? undefined : tokenPdsEndpoint;
···
55
55
56
56
// For bsky.network endpoints, use the default AUTH_SERVER (bsky.social)
57
57
// For other PDS servers, use their actual endpoint
58
58
-
let authUrl, state, codeVerifier, keyPair;
58
58
+
let authUrl, state, codeVerifier, keyPair, authorizationEndpoint;
59
59
60
60
if (isBskyNetwork) {
61
61
console.log('Using standard Bluesky OAuth flow for bsky.network PDS');
···
65
65
state = authData.state;
66
66
codeVerifier = authData.codeVerifier;
67
67
keyPair = authData.keyPair;
68
68
+
authorizationEndpoint = authData.pdsEndpoint;
69
69
+
70
70
+
console.log('Standard OAuth flow details:', {
71
71
+
pdsType: 'bsky.network',
72
72
+
authEndpoint: authorizationEndpoint,
73
73
+
statePrefix: state.substring(0, 5) + '...',
74
74
+
codeVerifierLength: codeVerifier.length
75
75
+
});
68
76
} else {
69
77
console.log('Using custom PDS OAuth flow for:', pdsEndpoint);
70
78
// Use the custom PDS endpoint for OAuth
···
73
81
state = authData.state;
74
82
codeVerifier = authData.codeVerifier;
75
83
keyPair = authData.keyPair;
84
84
+
authorizationEndpoint = authData.pdsEndpoint;
85
85
+
86
86
+
console.log('Custom PDS OAuth flow details:', {
87
87
+
pdsType: 'third-party',
88
88
+
pdsEndpoint,
89
89
+
authEndpoint: authorizationEndpoint,
90
90
+
statePrefix: state.substring(0, 5) + '...',
91
91
+
codeVerifierLength: codeVerifier.length,
92
92
+
redirectUri: 'https://flushes.app/auth/callback' // Expected to be this
93
93
+
});
76
94
}
77
95
78
96
// Store auth state