refactor: remove tokenHasExpiredDueToClockOutOfSync$ and re-sync clock + refresh token on validation failure

Instead of having a separate observable that detects token expiration due to clock
drift and immediately logs the user out, the oauthErrorEventOccurDueToClockOutOfSync$
now re-syncs the clock offset and attempts a token refresh when clock drift is detected.
Only if the refresh fails after re-syncing does it propagate the error and trigger logout.
This commit is contained in:
copilot-swe-agent[bot]
2026-07-03 11:45:21 +00:00
committed by GitHub
parent 31d7396c71
commit dc214beb29
@@ -29,12 +29,12 @@ import {
OAuthLogger
} from 'angular-oauth2-oidc';
import { WebCryptoJwksValidationHandler } from './web-crypto-jwks-validation-handler';
import { from, Observable, race, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, filter, map, scan, shareReplay, skip, switchMap, take } from 'rxjs/operators';
import { from, Observable, of, race, ReplaySubject } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, scan, shareReplay, switchMap, take } from 'rxjs/operators';
import { AuthService } from './auth.service';
import { AUTH_MODULE_CONFIG, AuthModuleConfig } from './auth-config';
import { RetryLoginService } from './retry-login.service';
import { TimeSyncService } from '../services/time-sync.service';
import { TimeSync, TimeSyncService } from '../services/time-sync.service';
const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => value && typeof (value as Promise<T>).then === 'function';
@@ -89,15 +89,11 @@ export class RedirectAuthService extends AuthService {
*/
secondTokenRefreshErrorEventOccur$: Observable<OAuthErrorEvent>;
/**
* Observable that emits an error when the token has expired due to
* the local machine clock being out of sync with the server time.
*/
tokenHasExpiredDueToClockOutOfSync$: Observable<Error>;
/**
* Observable that emits an error when the OAuth error event occurs due to
* the local machine clock being out of sync with the server time.
* When clock drift is detected, it re-syncs the clock and requests a new token
* before propagating the error (if the refresh still fails).
*/
oauthErrorEventOccurDueToClockOutOfSync$: Observable<Error>;
@@ -192,6 +188,15 @@ export class RedirectAuthService extends AuthService {
map(({ event }) => event as OAuthErrorEvent),
switchMap(() => this._timeSyncService.checkTimeSync(this.oauthService.clockSkewInSec)),
filter((timeSync) => timeSync?.outOfSync),
// Re-sync the clock with the detected server time and attempt a token refresh
switchMap((timeSync) =>
this._timeSyncService.syncClockOffset().pipe(
switchMap(() => from(this.oauthService.refreshToken())),
map(() => null as TimeSync | null),
catchError(() => of(timeSync))
)
),
filter((timeSync): timeSync is TimeSync => timeSync !== null),
map(
(timeSync) =>
new Error(
@@ -207,24 +212,6 @@ export class RedirectAuthService extends AuthService {
shareReplay(1)
);
this.tokenHasExpiredDueToClockOutOfSync$ = this.oauthService.events.pipe(
map(() => !!this.oauthService.getIdentityClaims() && this.tokenHasExpired()),
filter((hasExpired) => hasExpired),
// Skip the first occurrence: the library will attempt an automatic token
// refresh. Only check for clock drift if the token is still expired after
// that refresh attempt has had a chance to run.
skip(1),
switchMap(() => this._timeSyncService.checkTimeSync(this.oauthService.clockSkewInSec)),
filter((timeSync) => timeSync?.outOfSync),
map(
(timeSync) =>
new Error(
`Token has expired due to local machine clock ${timeSync.localDateTimeISO} being out of sync with server time ${timeSync.serverDateTimeISO}`
)
),
take(1)
);
this.onLogout$ = this.oauthService.events.pipe(
filter((event) => event.type === 'logout'),
map(() => undefined)
@@ -233,7 +220,6 @@ export class RedirectAuthService extends AuthService {
this.combinedOAuthErrorsStream$ = race([
this.oauthErrorEventOccurDueToClockOutOfSync$,
this.firstOauthErrorEventExcludingTokenRefreshError$,
this.tokenHasExpiredDueToClockOutOfSync$,
this.secondTokenRefreshErrorEventOccur$
]);