···
22
22
23
23
export default function Home() {
24
24
const router = useRouter();
25
25
-
const { isAuthenticated, accessToken, did, handle, serializedKeyPair, dpopNonce, pdsEndpoint, clearAuth } = useAuth();
25
25
+
const { isAuthenticated, accessToken, refreshToken, did, handle, serializedKeyPair, dpopNonce, pdsEndpoint, clearAuth } = useAuth();
26
26
27
27
// Status update state
28
28
const [text, setText] = useState('is ');
···
130
130
);
131
131
const keyPair = { publicKey, privateKey };
132
132
133
133
-
// First, check if auth is valid
133
133
+
// First, check if auth is valid (passing the refresh token)
134
134
const isAuthValid = await checkAuth(
135
135
accessToken,
136
136
keyPair,
137
137
did,
138
138
dpopNonce || null,
139
139
-
pdsEndpoint
139
139
+
pdsEndpoint,
140
140
+
refreshToken // Pass the refresh token to enable auto-refresh if needed
140
141
);
141
142
142
143
if (!isAuthValid) {
···
167
168
formattedText,
168
169
selectedEmoji,
169
170
dpopNonce || null,
170
170
-
pdsEndpoint
171
171
+
pdsEndpoint,
172
172
+
0, // initial retry count
173
173
+
refreshToken // Pass refresh token to enable token refresh if needed
171
174
);
172
175
173
176
console.log('Status update result:', result);
···
14
14
createdAt: string;
15
15
}
16
16
17
17
+
// Check if a JWT token is expired
18
18
+
export function isTokenExpired(token: string): boolean {
19
19
+
try {
20
20
+
// Extract the payload from the JWT token
21
21
+
const parts = token.split('.');
22
22
+
if (parts.length !== 3) {
23
23
+
console.error('Invalid token format');
24
24
+
return true; // Assume expired if format is invalid
25
25
+
}
26
26
+
27
27
+
// Decode the payload
28
28
+
const payload = JSON.parse(atob(parts[1]));
29
29
+
30
30
+
// Check if the token has an expiration time
31
31
+
if (!payload.exp) {
32
32
+
console.warn('Token does not have an expiration time');
33
33
+
return false; // Can't determine if it's expired
34
34
+
}
35
35
+
36
36
+
// Check if the token is expired
37
37
+
const now = Math.floor(Date.now() / 1000);
38
38
+
const isExpired = payload.exp <= now;
39
39
+
40
40
+
if (isExpired) {
41
41
+
console.log(`Token expired at ${new Date(payload.exp * 1000).toISOString()}`);
42
42
+
}
43
43
+
44
44
+
return isExpired;
45
45
+
} catch (error) {
46
46
+
console.error('Error checking token expiration:', error);
47
47
+
return true; // Assume expired if there's an error
48
48
+
}
49
49
+
}
50
50
+
51
51
+
// Refresh an access token using the refresh token
52
52
+
export async function refreshAccessToken(
53
53
+
refreshToken: string,
54
54
+
keyPair: CryptoKeyPair,
55
55
+
pdsEndpoint: string
56
56
+
): Promise<{
57
57
+
accessToken: string;
58
58
+
refreshToken: string;
59
59
+
dpopNonce?: string;
60
60
+
}> {
61
61
+
try {
62
62
+
if (!pdsEndpoint) {
63
63
+
throw new Error('No PDS endpoint provided for token refresh');
64
64
+
}
65
65
+
66
66
+
console.log('Refreshing access token with PDS endpoint:', pdsEndpoint);
67
67
+
68
68
+
// Endpoint for token refresh
69
69
+
const tokenEndpoint = `${pdsEndpoint}/oauth/token`;
70
70
+
71
71
+
// Get a nonce for the DPoP token
72
72
+
let dpopNonce = null;
73
73
+
try {
74
74
+
// Try to get a nonce with a HEAD request
75
75
+
const headResponse = await fetch(tokenEndpoint, {
76
76
+
method: 'HEAD'
77
77
+
});
78
78
+
79
79
+
dpopNonce = headResponse.headers.get('DPoP-Nonce');
80
80
+
if (dpopNonce) {
81
81
+
console.log('Got nonce for token refresh:', dpopNonce);
82
82
+
}
83
83
+
} catch (nonceError) {
84
84
+
console.warn('Failed to get nonce for token refresh:', nonceError);
85
85
+
}
86
86
+
87
87
+
// Generate DPoP token for the refresh request
88
88
+
const publicKey = await exportJWK(keyPair.publicKey);
89
89
+
const dpopToken = await generateDPoPToken(
90
90
+
keyPair.privateKey,
91
91
+
publicKey,
92
92
+
'POST',
93
93
+
tokenEndpoint,
94
94
+
dpopNonce
95
95
+
);
96
96
+
97
97
+
// Make the token refresh request
98
98
+
const response = await fetch(tokenEndpoint, {
99
99
+
method: 'POST',
100
100
+
headers: {
101
101
+
'Content-Type': 'application/x-www-form-urlencoded',
102
102
+
'DPoP': dpopToken
103
103
+
},
104
104
+
body: new URLSearchParams({
105
105
+
'grant_type': 'refresh_token',
106
106
+
'refresh_token': refreshToken,
107
107
+
'client_id': 'https://flushing.im/client-metadata.json'
108
108
+
})
109
109
+
});
110
110
+
111
111
+
// Check for nonce error
112
112
+
if (response.status === 401) {
113
113
+
const newNonce = response.headers.get('DPoP-Nonce');
114
114
+
if (newNonce) {
115
115
+
console.log('Got new nonce during token refresh:', newNonce);
116
116
+
117
117
+
// Try again with the new nonce
118
118
+
const retryDpopToken = await generateDPoPToken(
119
119
+
keyPair.privateKey,
120
120
+
publicKey,
121
121
+
'POST',
122
122
+
tokenEndpoint,
123
123
+
newNonce
124
124
+
);
125
125
+
126
126
+
const retryResponse = await fetch(tokenEndpoint, {
127
127
+
method: 'POST',
128
128
+
headers: {
129
129
+
'Content-Type': 'application/x-www-form-urlencoded',
130
130
+
'DPoP': retryDpopToken
131
131
+
},
132
132
+
body: new URLSearchParams({
133
133
+
'grant_type': 'refresh_token',
134
134
+
'refresh_token': refreshToken,
135
135
+
'client_id': 'https://flushing.im/client-metadata.json'
136
136
+
})
137
137
+
});
138
138
+
139
139
+
if (!retryResponse.ok) {
140
140
+
const errorText = await retryResponse.text();
141
141
+
throw new Error(`Token refresh retry failed: ${retryResponse.status}, ${errorText}`);
142
142
+
}
143
143
+
144
144
+
const tokenData = await retryResponse.json();
145
145
+
146
146
+
// Return the new tokens and nonce
147
147
+
return {
148
148
+
accessToken: tokenData.access_token,
149
149
+
refreshToken: tokenData.refresh_token || refreshToken, // Use the new refresh token if provided
150
150
+
dpopNonce: newNonce
151
151
+
};
152
152
+
}
153
153
+
}
154
154
+
155
155
+
if (!response.ok) {
156
156
+
const errorText = await response.text();
157
157
+
throw new Error(`Token refresh failed: ${response.status}, ${errorText}`);
158
158
+
}
159
159
+
160
160
+
const tokenData = await response.json();
161
161
+
162
162
+
// Get any nonce from the response headers
163
163
+
const responseNonce = response.headers.get('DPoP-Nonce');
164
164
+
165
165
+
// Return the new tokens and nonce
166
166
+
return {
167
167
+
accessToken: tokenData.access_token,
168
168
+
refreshToken: tokenData.refresh_token || refreshToken, // Use the new refresh token if provided
169
169
+
dpopNonce: responseNonce || dpopNonce
170
170
+
};
171
171
+
} catch (error) {
172
172
+
console.error('Error refreshing access token:', error);
173
173
+
throw error;
174
174
+
}
175
175
+
}
176
176
+
17
177
// Check if authentication is valid by making a simple request
18
178
export async function checkAuth(
19
179
accessToken: string,
20
180
keyPair: CryptoKeyPair,
21
21
-
did: string, // Add DID parameter
181
181
+
did: string,
22
182
dpopNonce: string | null = null,
23
23
-
pdsEndpoint: string | null = null
183
183
+
pdsEndpoint: string | null = null,
184
184
+
refreshToken: string | null = null // Add refresh token parameter
24
185
): Promise<boolean> {
25
186
try {
26
187
if (!pdsEndpoint) {
···
31
192
if (!did) {
32
193
console.error('No DID provided for auth check');
33
194
return false;
195
195
+
}
196
196
+
197
197
+
// First check if the token is expired by decoding it
198
198
+
const tokenExpired = isTokenExpired(accessToken);
199
199
+
if (tokenExpired && refreshToken) {
200
200
+
console.log('Access token is expired, attempting to refresh...');
201
201
+
202
202
+
try {
203
203
+
// Try to refresh the token
204
204
+
const { accessToken: newAccessToken, refreshToken: newRefreshToken, dpopNonce: newNonce } =
205
205
+
await refreshAccessToken(refreshToken, keyPair, pdsEndpoint);
206
206
+
207
207
+
// Update tokens in localStorage
208
208
+
localStorage.setItem('accessToken', newAccessToken);
209
209
+
localStorage.setItem('refreshToken', newRefreshToken);
210
210
+
if (newNonce) localStorage.setItem('dpopNonce', newNonce);
211
211
+
212
212
+
console.log('Token refreshed successfully, retrying auth check with new token');
213
213
+
214
214
+
// Return the result of checkAuth with the new token
215
215
+
return checkAuth(newAccessToken, keyPair, did, newNonce || null, pdsEndpoint, newRefreshToken);
216
216
+
} catch (refreshError) {
217
217
+
console.error('Token refresh failed:', refreshError);
218
218
+
return false;
219
219
+
}
34
220
}
35
221
36
222
console.log('Checking auth with PDS endpoint:', pdsEndpoint);
···
88
274
if (nonce) {
89
275
console.log('Got nonce during auth check:', nonce);
90
276
// Try again with the nonce, but prevent infinite recursion
91
91
-
return checkAuth(accessToken, keyPair, did, nonce, pdsEndpoint);
277
277
+
return checkAuth(accessToken, keyPair, did, nonce, pdsEndpoint, refreshToken);
278
278
+
}
279
279
+
280
280
+
// If we have a refresh token, try to refresh the access token
281
281
+
if (refreshToken && !tokenExpired) { // Only try this if we didn't already try above
282
282
+
console.log('Auth failed with 401, attempting to refresh token...');
283
283
+
284
284
+
try {
285
285
+
// Try to refresh the token
286
286
+
const { accessToken: newAccessToken, refreshToken: newRefreshToken, dpopNonce: newNonce } =
287
287
+
await refreshAccessToken(refreshToken, keyPair, pdsEndpoint);
288
288
+
289
289
+
// Update tokens in localStorage
290
290
+
localStorage.setItem('accessToken', newAccessToken);
291
291
+
localStorage.setItem('refreshToken', newRefreshToken);
292
292
+
if (newNonce) localStorage.setItem('dpopNonce', newNonce);
293
293
+
294
294
+
console.log('Token refreshed successfully, retrying auth check with new token');
295
295
+
296
296
+
// Return the result of checkAuth with the new token
297
297
+
return checkAuth(newAccessToken, keyPair, did, newNonce || null, pdsEndpoint, newRefreshToken);
298
298
+
} catch (refreshError) {
299
299
+
console.error('Token refresh failed:', refreshError);
300
300
+
}
92
301
}
93
302
}
94
303
···
329
538
emoji: string,
330
539
dpopNonce: string | null = null,
331
540
pdsEndpoint: string | null = null,
332
332
-
retryCount: number = 0 // Add retry counter
541
541
+
retryCount: number = 0, // Add retry counter
542
542
+
refreshToken: string | null = null // Add refresh token parameter
333
543
): Promise<any> {
334
544
// Safety check: prevent infinite recursion
335
545
if (retryCount >= 3) {
···
418
628
emoji,
419
629
errorData.nonce,
420
630
pdsEndpoint,
421
421
-
retryCount + 1
631
631
+
retryCount + 1,
632
632
+
refreshToken // Pass through the refresh token
422
633
);
634
634
+
}
635
635
+
636
636
+
// Handle expired token with refresh
637
637
+
if (response.status === 401 && refreshToken) {
638
638
+
console.log('Authentication error (401), attempting token refresh...');
639
639
+
640
640
+
try {
641
641
+
// Try to refresh the token
642
642
+
const { accessToken: newAccessToken, refreshToken: newRefreshToken, dpopNonce: newNonce } =
643
643
+
await refreshAccessToken(refreshToken, keyPair, pdsEndpoint || 'https://bsky.social');
644
644
+
645
645
+
// Update tokens in localStorage
646
646
+
if (typeof localStorage !== 'undefined') {
647
647
+
localStorage.setItem('accessToken', newAccessToken);
648
648
+
localStorage.setItem('refreshToken', newRefreshToken);
649
649
+
if (newNonce) localStorage.setItem('dpopNonce', newNonce);
650
650
+
}
651
651
+
652
652
+
console.log('Token refreshed successfully, retrying status creation');
653
653
+
654
654
+
// Retry with the new token
655
655
+
return createFlushingStatus(
656
656
+
newAccessToken,
657
657
+
keyPair,
658
658
+
did,
659
659
+
statusText,
660
660
+
emoji,
661
661
+
newNonce || null,
662
662
+
pdsEndpoint,
663
663
+
retryCount + 1,
664
664
+
newRefreshToken
665
665
+
);
666
666
+
} catch (refreshError) {
667
667
+
console.error('Token refresh failed:', refreshError);
668
668
+
throw new Error('Authentication expired. Please log out and log in again.');
669
669
+
}
423
670
}
424
671
425
672
// For other errors, throw with more details