1
1
import { Logger } from "../logging.js" ;
2
2
import { LocalSyncState } from "./local_state.js" ;
3
- import { AuthError , Transition } from "./protocol.js" ;
3
+ import { AuthError , IdentityVersion , Transition } from "./protocol.js" ;
4
4
import jwtDecode from "jwt-decode" ;
5
5
6
6
// setTimout uses 32 bit integer, so it can only
7
7
// schedule about 24 days in the future.
8
8
const MAXIMUM_REFRESH_DELAY = 20 * 24 * 60 * 60 * 1000 ; // 20 days
9
9
10
+ const MAX_TOKEN_CONFIRMATION_ATTEMPTS = 2 ;
11
+
10
12
/**
11
13
* An async function returning the JWT-encoded OpenID Connect Identity Token
12
14
* if available.
@@ -83,21 +85,24 @@ export class AuthenticationManager {
83
85
// Shared by the BaseClient so that the auth manager can easily inspect it
84
86
private readonly syncState : LocalSyncState ;
85
87
// Passed down by BaseClient, sends a message to the server
86
- private readonly authenticate : ( token : string ) => void ;
88
+ private readonly authenticate : ( token : string ) => IdentityVersion ;
87
89
private readonly stopSocket : ( ) => Promise < void > ;
88
- private readonly restartSocket : ( ) => void ;
90
+ private readonly tryRestartSocket : ( ) => void ;
89
91
private readonly pauseSocket : ( ) => void ;
90
92
private readonly resumeSocket : ( ) => void ;
91
93
// Passed down by BaseClient, sends a message to the server
92
94
private readonly clearAuth : ( ) => void ;
93
95
private readonly logger : Logger ;
94
96
private readonly refreshTokenLeewaySeconds : number ;
97
+ // Number of times we have attempted to confirm the latest token. We retry up
98
+ // to `MAX_TOKEN_CONFIRMATION_ATTEMPTS` times.
99
+ private tokenConfirmationAttempts = 0 ;
95
100
constructor (
96
101
syncState : LocalSyncState ,
97
102
callbacks : {
98
- authenticate : ( token : string ) => void ;
103
+ authenticate : ( token : string ) => IdentityVersion ;
99
104
stopSocket : ( ) => Promise < void > ;
100
- restartSocket : ( ) => void ;
105
+ tryRestartSocket : ( ) => void ;
101
106
pauseSocket : ( ) => void ;
102
107
resumeSocket : ( ) => void ;
103
108
clearAuth : ( ) => void ;
@@ -110,7 +115,7 @@ export class AuthenticationManager {
110
115
this . syncState = syncState ;
111
116
this . authenticate = callbacks . authenticate ;
112
117
this . stopSocket = callbacks . stopSocket ;
113
- this . restartSocket = callbacks . restartSocket ;
118
+ this . tryRestartSocket = callbacks . tryRestartSocket ;
114
119
this . pauseSocket = callbacks . pauseSocket ;
115
120
this . resumeSocket = callbacks . resumeSocket ;
116
121
this . clearAuth = callbacks . clearAuth ;
@@ -138,8 +143,6 @@ export class AuthenticationManager {
138
143
hasRetried : false ,
139
144
} ) ;
140
145
this . authenticate ( token . value ) ;
141
- this . _logVerbose ( "resuming WS after auth token fetch" ) ;
142
- this . resumeSocket ( ) ;
143
146
} else {
144
147
this . setAuthState ( {
145
148
state : "initialRefetch" ,
@@ -148,6 +151,8 @@ export class AuthenticationManager {
148
151
// Try again with `forceRefreshToken: true`
149
152
await this . refetchToken ( ) ;
150
153
}
154
+ this . _logVerbose ( "resuming WS after auth token fetch" ) ;
155
+ this . resumeSocket ( ) ;
151
156
}
152
157
153
158
onTransition ( serverMessage : Transition ) {
@@ -176,13 +181,24 @@ export class AuthenticationManager {
176
181
if ( this . authState . state === "waitingForServerConfirmationOfFreshToken" ) {
177
182
this . _logVerbose ( "server confirmed new auth token is valid" ) ;
178
183
this . scheduleTokenRefetch ( this . authState . token ) ;
184
+ this . tokenConfirmationAttempts = 0 ;
179
185
if ( ! this . authState . hadAuth ) {
180
186
this . authState . config . onAuthChange ( true ) ;
181
187
}
182
188
}
183
189
}
184
190
185
191
onAuthError ( serverMessage : AuthError ) {
192
+ // If the AuthError is not due to updating the token, and we're currently
193
+ // waiting on the result of a token update, ignore.
194
+ if (
195
+ serverMessage . authUpdateAttempted === false &&
196
+ ( this . authState . state === "waitingForServerConfirmationOfFreshToken" ||
197
+ this . authState . state === "waitingForServerConfirmationOfCachedToken" )
198
+ ) {
199
+ this . _logVerbose ( "ignoring non-auth token expired error" ) ;
200
+ return ;
201
+ }
186
202
const { baseVersion } = serverMessage ;
187
203
// Versioned AuthErrors are ignored if the client advanced to
188
204
// a newer auth identity
@@ -201,12 +217,14 @@ export class AuthenticationManager {
201
217
// in that we pause the WebSocket so that mutations
202
218
// don't retry with bad auth.
203
219
private async tryToReauthenticate ( serverMessage : AuthError ) {
204
- // We only retry once, to avoid infinite retries
220
+ this . _logVerbose ( `attempting to reauthenticate: ${ serverMessage . error } ` ) ;
205
221
if (
206
222
// No way to fetch another token, kaboom
207
223
this . authState . state === "noAuth" ||
208
- // We failed on a fresh token, trying another one won't help
209
- this . authState . state === "waitingForServerConfirmationOfFreshToken"
224
+ // We failed on a fresh token. After a small number of retries, we give up
225
+ // and clear the auth state to avoid infinite retries.
226
+ ( this . authState . state === "waitingForServerConfirmationOfFreshToken" &&
227
+ this . tokenConfirmationAttempts >= MAX_TOKEN_CONFIRMATION_ATTEMPTS )
210
228
) {
211
229
this . logger . error (
212
230
`Failed to authenticate: "${ serverMessage . error } ", check your server auth config` ,
@@ -219,7 +237,13 @@ export class AuthenticationManager {
219
237
}
220
238
return ;
221
239
}
222
- this . _logVerbose ( "attempting to reauthenticate" ) ;
240
+ if ( this . authState . state === "waitingForServerConfirmationOfFreshToken" ) {
241
+ this . tokenConfirmationAttempts ++ ;
242
+ this . _logVerbose (
243
+ `retrying reauthentication, ${ MAX_TOKEN_CONFIRMATION_ATTEMPTS - this . tokenConfirmationAttempts } attempts remaining` ,
244
+ ) ;
245
+ }
246
+
223
247
await this . stopSocket ( ) ;
224
248
const token = await this . fetchTokenAndGuardAgainstRace (
225
249
this . authState . config . fetchToken ,
@@ -248,7 +272,7 @@ export class AuthenticationManager {
248
272
}
249
273
this . setAndReportAuthFailed ( this . authState . config . onAuthChange ) ;
250
274
}
251
- this . restartSocket ( ) ;
275
+ this . tryRestartSocket ( ) ;
252
276
}
253
277
254
278
// Force refetch the token and schedule another refetch
@@ -291,12 +315,12 @@ export class AuthenticationManager {
291
315
}
292
316
this . setAndReportAuthFailed ( this . authState . config . onAuthChange ) ;
293
317
}
294
- // Resuming in case this refetch was triggered
295
- // by an invalid cached token .
318
+ // Restart in case this refetch was triggered via schedule during
319
+ // a reauthentication attempt .
296
320
this . _logVerbose (
297
- "resuming WS after auth token fetch (if currently paused )" ,
321
+ "restarting WS after auth token fetch (if currently stopped )" ,
298
322
) ;
299
- this . resumeSocket ( ) ;
323
+ this . tryRestartSocket ( ) ;
300
324
}
301
325
302
326
private scheduleTokenRefetch ( token : string ) {
@@ -348,6 +372,7 @@ export class AuthenticationManager {
348
372
delay = 0 ;
349
373
}
350
374
const refetchTokenTimeoutId = setTimeout ( ( ) => {
375
+ this . _logVerbose ( "running scheduled token refetch" ) ;
351
376
void this . refetchToken ( ) ;
352
377
} , delay ) ;
353
378
this . setAuthState ( {
@@ -369,9 +394,15 @@ export class AuthenticationManager {
369
394
} ,
370
395
) {
371
396
const originalConfigVersion = ++ this . configVersion ;
397
+ this . _logVerbose (
398
+ `fetching token with config version ${ originalConfigVersion } ` ,
399
+ ) ;
372
400
const token = await fetchToken ( fetchArgs ) ;
373
401
if ( this . configVersion !== originalConfigVersion ) {
374
402
// This is a stale config
403
+ this . _logVerbose (
404
+ `stale config version, expected ${ originalConfigVersion } , got ${ this . configVersion } ` ,
405
+ ) ;
375
406
return { isFromOutdatedConfig : true } ;
376
407
}
377
408
return { isFromOutdatedConfig : false , value : token } ;
@@ -381,6 +412,7 @@ export class AuthenticationManager {
381
412
this . resetAuthState ( ) ;
382
413
// Bump this in case we are mid-token-fetch when we get stopped
383
414
this . configVersion ++ ;
415
+ this . _logVerbose ( `config version bumped to ${ this . configVersion } ` ) ;
384
416
}
385
417
386
418
private setAndReportAuthFailed (
@@ -395,6 +427,31 @@ export class AuthenticationManager {
395
427
}
396
428
397
429
private setAuthState ( newAuth : AuthState ) {
430
+ const authStateForLog =
431
+ newAuth . state === "waitingForServerConfirmationOfFreshToken"
432
+ ? {
433
+ hadAuth : newAuth . hadAuth ,
434
+ state : newAuth . state ,
435
+ token : `...${ newAuth . token . slice ( - 7 ) } ` ,
436
+ }
437
+ : { state : newAuth . state } ;
438
+ this . _logVerbose (
439
+ `setting auth state to ${ JSON . stringify ( authStateForLog ) } ` ,
440
+ ) ;
441
+ switch ( newAuth . state ) {
442
+ case "waitingForScheduledRefetch" :
443
+ case "notRefetching" :
444
+ case "noAuth" :
445
+ this . tokenConfirmationAttempts = 0 ;
446
+ break ;
447
+ case "waitingForServerConfirmationOfFreshToken" :
448
+ case "waitingForServerConfirmationOfCachedToken" :
449
+ case "initialRefetch" :
450
+ break ;
451
+ default : {
452
+ const _typeCheck : never = newAuth ;
453
+ }
454
+ }
398
455
if ( this . authState . state === "waitingForScheduledRefetch" ) {
399
456
clearTimeout ( this . authState . refetchTokenTimeoutId ) ;
400
457
0 commit comments