AAE-24081 Fix refresh token error with multiple tabs opened (#9964)

* Fix refresh token error with multiple opened tabs (kwnown angular-oauth2-oidc issue => https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850)

* fix typo

* AAE-24081 test silent refresh and token refresh are called when automatic silent refresh is setup
This commit is contained in:
Amedeo Lepore
2024-07-23 17:15:36 +02:00
committed by GitHub
parent 63fa673709
commit 4c8e53d983
2 changed files with 78 additions and 7 deletions

View File

@@ -16,7 +16,7 @@
*/
import { TestBed } from '@angular/core/testing';
import { OAuthService, OAuthEvent, OAuthStorage, AUTH_CONFIG } from 'angular-oauth2-oidc';
import { OAuthService, OAuthEvent, OAuthStorage, AUTH_CONFIG, TokenResponse } from 'angular-oauth2-oidc';
import { Subject } from 'rxjs';
import { RedirectAuthService } from './redirect-auth.service';
import { AUTH_MODULE_CONFIG } from './auth-config';
@@ -31,7 +31,13 @@ describe('RedirectAuthService', () => {
const oauthEvents$ = new Subject<OAuthEvent>();
const mockOauthService: Partial<OAuthService> = {
clearHashAfterLogin: false,
events: oauthEvents$
events: oauthEvents$,
configure: () => {},
hasValidAccessToken: jasmine.createSpy().and.returnValue(true),
setupAutomaticSilentRefresh: () => {
mockOauthService.silentRefresh();
mockOauthService.refreshToken();
}
};
beforeEach(() => {
@@ -45,8 +51,10 @@ describe('RedirectAuthService', () => {
]
});
service = TestBed.inject(RedirectAuthService);
TestBed.inject(OAuthService);
service = TestBed.inject(RedirectAuthService);
spyOn(service, 'ensureDiscoveryDocument').and.resolveTo(true);
mockOauthService.getAccessToken = () => 'access-token';
});
it('should emit event when token_received event is received', () => {
@@ -66,4 +74,23 @@ describe('RedirectAuthService', () => {
expect(onTokenReceivedSpy).not.toHaveBeenCalled();
});
it('should call refresh token and silent refresh when automatic silent refresh is setup', async () => {
let refreshTokenCalled = false;
let silentRefreshCalled = false;
mockOauthService.refreshToken = async () => {
refreshTokenCalled = true;
return Promise.resolve({} as TokenResponse);
};
mockOauthService.silentRefresh = async () => {
silentRefreshCalled = true;
return Promise.resolve({} as OAuthEvent);
};
await service.init();
expect(refreshTokenCalled).toBe(true);
expect(silentRefreshCalled).toBe(true);
});
});

View File

@@ -16,7 +16,7 @@
*/
import { Inject, Injectable, inject } from '@angular/core';
import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthEvent, OAuthService, OAuthStorage, TokenResponse, LoginOptions } from 'angular-oauth2-oidc';
import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthEvent, OAuthService, OAuthStorage, TokenResponse, LoginOptions, OAuthSuccessEvent } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { from, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
@@ -169,13 +169,57 @@ export class RedirectAuthService extends AuthService {
});
}
return this.ensureDiscoveryDocument().then(() =>
void this.oauthService.setupAutomaticSilentRefresh()
).catch(() => {
return this.ensureDiscoveryDocument().then(() => {
this.oauthService.setupAutomaticSilentRefresh();
return void this.allowRefreshTokenAndSilentRefreshOnMultipleTabs();
}).catch(() => {
// catch error to prevent the app from crashing when trying to access unprotected routes
});
}
/**
* Fix a known issue (https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850)
* where multiple tabs can cause the token refresh and the silent refresh to fail.
* This patch is based on the solutions provided in the following comments:
* https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850#issuecomment-889921776 fix silent refresh for the implicit flow
* https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850#issuecomment-1557286966 fix refresh token for the code flow
*/
private allowRefreshTokenAndSilentRefreshOnMultipleTabs() {
let lastUpdatedAccessToken: string | undefined;
if (this.oauthService.hasValidAccessToken()) {
lastUpdatedAccessToken = this.oauthService.getAccessToken();
}
const originalRefreshToken = this.oauthService.refreshToken.bind(this.oauthService);
this.oauthService.refreshToken = (): Promise<TokenResponse> =>
navigator.locks.request(`refresh_tokens_${location.origin}`, () => {
if (!!lastUpdatedAccessToken && lastUpdatedAccessToken !== this.oauthService.getAccessToken()) {
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_received'));
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
lastUpdatedAccessToken = this.oauthService.getAccessToken();
return;
}
return originalRefreshToken().then((resp) => (lastUpdatedAccessToken = resp.access_token));
});
const originalSilentRefresh = this.oauthService.silentRefresh.bind(this.oauthService);
this.oauthService.silentRefresh = async (params: any = {}, noPrompt = true): Promise<OAuthEvent> =>
navigator.locks.request(`silent_refresh_${location.origin}`, async (): Promise<OAuthEvent> => {
if (lastUpdatedAccessToken !== this.oauthService.getAccessToken()) {
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_received'));
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
const event = new OAuthSuccessEvent('silently_refreshed');
(this.oauthService as any).eventsSubject.next(event);
lastUpdatedAccessToken = this.oauthService.getAccessToken();
return event;
} else {
return originalSilentRefresh(params, noPrompt);
}
});
}
updateIDPConfiguration(config: AuthConfig) {
this.oauthService.configure(config);
}