From 1309040ae5ec807aa5b4bc739c918a396b5d890e Mon Sep 17 00:00:00 2001 From: FabianGosebrink Date: Sat, 30 May 2026 18:04:36 +0200 Subject: [PATCH] fix: reset abandoned code flow flag on non-callback checkAuth When a code flow is started via authorize() but abandoned (the user returns to the app without the authorization callback URL), storageCodeFlowInProgress was left true permanently, because resetCodeFlowInProgress is only called from the code-flow callback path. If the session is then established via checkAuthIncludingServer -> forceRefreshSession (iframe silent renew), shouldStartPeriodicallyCheckForConfig keeps returning false, so tokens are never silently renewed. Reset the flag in checkAuthWithConfig whenever the current URL is not a callback: there is no in-flight code flow to protect in that case. Fixes #2221 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/auth-state/check-auth.service.spec.ts | 1853 +++++++++-------- .../src/lib/auth-state/check-auth.service.ts | 10 + 2 files changed, 960 insertions(+), 903 deletions(-) diff --git a/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.spec.ts index 34310d60a..2c7d5b825 100644 --- a/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.spec.ts @@ -1,903 +1,950 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { of, throwError } from 'rxjs'; -import { mockAbstractProvider, mockProvider } from '../../test/auto-mock'; -import { AutoLoginService } from '../auto-login/auto-login.service'; -import { CallbackService } from '../callback/callback.service'; -import { PeriodicallyTokenCheckService } from '../callback/periodically-token-check.service'; -import { RefreshSessionService } from '../callback/refresh-session.service'; -import { - StsConfigLoader, - StsConfigStaticLoader, -} from '../config/loader/config-loader'; -import { OpenIdConfiguration } from '../config/openid-configuration'; -import { CallbackContext } from '../flows/callback-context'; -import { CheckSessionService } from '../iframe/check-session.service'; -import { SilentRenewService } from '../iframe/silent-renew.service'; -import { LoggerService } from '../logging/logger.service'; -import { LoginResponse } from '../login/login-response'; -import { PopUpService } from '../login/popup/popup.service'; -import { EventTypes } from '../public-events/event-types'; -import { PublicEventsService } from '../public-events/public-events.service'; -import { StoragePersistenceService } from '../storage/storage-persistence.service'; -import { UserService } from '../user-data/user.service'; -import { CurrentUrlService } from '../utils/url/current-url.service'; -import { AuthStateService } from './auth-state.service'; -import { CheckAuthService } from './check-auth.service'; - -describe('CheckAuthService', () => { - let checkAuthService: CheckAuthService; - let authStateService: AuthStateService; - let userService: UserService; - let checkSessionService: CheckSessionService; - let callBackService: CallbackService; - let silentRenewService: SilentRenewService; - let periodicallyTokenCheckService: PeriodicallyTokenCheckService; - let refreshSessionService: RefreshSessionService; - let popUpService: PopUpService; - let autoLoginService: AutoLoginService; - let storagePersistenceService: StoragePersistenceService; - let currentUrlService: CurrentUrlService; - let publicEventsService: PublicEventsService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [RouterTestingModule], - providers: [ - mockProvider(CheckSessionService), - mockProvider(SilentRenewService), - mockProvider(UserService), - mockProvider(LoggerService), - mockProvider(AuthStateService), - mockProvider(CallbackService), - mockProvider(RefreshSessionService), - mockProvider(PeriodicallyTokenCheckService), - mockProvider(PopUpService), - mockProvider(CurrentUrlService), - mockProvider(PublicEventsService), - mockAbstractProvider(StsConfigLoader, StsConfigStaticLoader), - AutoLoginService, - mockProvider(StoragePersistenceService), - ], - }); - }); - - beforeEach(() => { - checkAuthService = TestBed.inject(CheckAuthService); - refreshSessionService = TestBed.inject(RefreshSessionService); - userService = TestBed.inject(UserService); - authStateService = TestBed.inject(AuthStateService); - checkSessionService = TestBed.inject(CheckSessionService); - callBackService = TestBed.inject(CallbackService); - silentRenewService = TestBed.inject(SilentRenewService); - periodicallyTokenCheckService = TestBed.inject( - PeriodicallyTokenCheckService - ); - popUpService = TestBed.inject(PopUpService); - autoLoginService = TestBed.inject(AutoLoginService); - storagePersistenceService = TestBed.inject(StoragePersistenceService); - currentUrlService = TestBed.inject(CurrentUrlService); - publicEventsService = TestBed.inject(PublicEventsService); - }); - - afterEach(() => { - storagePersistenceService.clear({} as OpenIdConfiguration); - }); - - it('should create', () => { - expect(checkAuthService).toBeTruthy(); - }); - - describe('checkAuth', () => { - it('uses config with matching state when url has state param and config with state param is stored', waitForAsync(() => { - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - 'the-state-param' - ); - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(storagePersistenceService, 'read') - .withArgs('authStateControl', allConfigs[0]) - .and.returnValue('the-state-param'); - const spy = spyOn( - checkAuthService as any, - 'checkAuthWithConfig' - ).and.callThrough(); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalledOnceWith( - allConfigs[0], - allConfigs, - undefined - ); - }); - })); - - it('throws error when url has state param and stored config with matching state param is not found', waitForAsync(() => { - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - 'the-state-param' - ); - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(storagePersistenceService, 'read') - .withArgs('authStateControl', allConfigs[0]) - .and.returnValue('not-matching-state-param'); - const spy = spyOn( - checkAuthService as any, - 'checkAuthWithConfig' - ).and.callThrough(); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe({ - error: (err) => { - expect(err).toBeTruthy(); - expect(spy).not.toHaveBeenCalled(); - }, - }); - })); - - it('uses first/default config when no param is passed', waitForAsync(() => { - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - null - ); - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - const spy = spyOn( - checkAuthService as any, - 'checkAuthWithConfig' - ).and.callThrough(); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalledOnceWith( - { configId: 'configId1', authority: 'some-authority' }, - allConfigs, - undefined - ); - }); - })); - - it('returns null and sendMessageToMainWindow if currently in a popup', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(popUpService as any, 'canAccessSessionStorage').and.returnValue( - true - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOnProperty(popUpService as any, 'windowInternal').and.returnValue({ - opener: {} as Window, - }); - spyOn(storagePersistenceService, 'read').and.returnValue(null); - - spyOn(popUpService, 'isCurrentlyInPopup').and.returnValue(true); - const popupSpy = spyOn(popUpService, 'sendMessageToMainWindow'); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: false, - errorMessage: '', - userData: null, - idToken: '', - accessToken: '', - configId: '', - }); - expect(popupSpy).toHaveBeenCalled(); - }); - })); - - it('returns isAuthenticated: false with error message in case handleCallbackAndFireEvents throws an error', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(true); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - - const spy = spyOn( - callBackService, - 'handleCallbackAndFireEvents' - ).and.returnValue(throwError(() => new Error('ERROR'))); - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: false, - errorMessage: 'ERROR', - configId: 'configId1', - idToken: '', - userData: null, - accessToken: '', - }); - expect(spy).toHaveBeenCalled(); - }); - })); - - it('calls callbackService.handlePossibleStsCallback with current url when callback is true', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(true); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'getAccessToken').and.returnValue('at'); - spyOn(authStateService, 'getIdToken').and.returnValue('idt'); - - const spy = spyOn( - callBackService, - 'handleCallbackAndFireEvents' - ).and.returnValue(of({} as CallbackContext)); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: true, - userData: undefined, - accessToken: 'at', - configId: 'configId1', - idToken: 'idt', - }); - expect(spy).toHaveBeenCalled(); - }); - })); - - it('does NOT call handleCallbackAndFireEvents with current url when callback is false', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - - const spy = spyOn( - callBackService, - 'handleCallbackAndFireEvents' - ).and.returnValue(of({} as CallbackContext)); - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'getAccessToken').and.returnValue('at'); - spyOn(authStateService, 'getIdToken').and.returnValue('idt'); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: true, - userData: undefined, - accessToken: 'at', - configId: 'configId1', - idToken: 'idt', - }); - expect(spy).not.toHaveBeenCalled(); - }); - })); - - it('does fire the auth and user data events when it is not a callback from the security token service and is authenticated', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(userService, 'getUserDataFromStore').and.returnValue({ - some: 'user-data', - }); - spyOn(authStateService, 'getAccessToken').and.returnValue('at'); - spyOn(authStateService, 'getIdToken').and.returnValue('idt'); - - const setAuthorizedAndFireEventSpy = spyOn( - authStateService, - 'setAuthenticatedAndFireEvent' - ); - const userServiceSpy = spyOn(userService, 'publishUserDataIfExists'); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: true, - userData: { - some: 'user-data', - }, - accessToken: 'at', - configId: 'configId1', - idToken: 'idt', - }); - expect(setAuthorizedAndFireEventSpy).toHaveBeenCalled(); - expect(userServiceSpy).toHaveBeenCalled(); - }); - })); - - it('does NOT fire the auth and user data events when it is not a callback from the security token service and is NOT authenticated', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - spyOn(authStateService, 'getAccessToken').and.returnValue('at'); - spyOn(authStateService, 'getIdToken').and.returnValue('it'); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - - const setAuthorizedAndFireEventSpy = spyOn( - authStateService, - 'setAuthenticatedAndFireEvent' - ); - const userServiceSpy = spyOn(userService, 'publishUserDataIfExists'); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: false, - userData: undefined, - accessToken: 'at', - configId: 'configId1', - idToken: 'it', - }); - expect(setAuthorizedAndFireEventSpy).not.toHaveBeenCalled(); - expect(userServiceSpy).not.toHaveBeenCalled(); - }); - })); - - it('if authenticated return true', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'getAccessToken').and.returnValue('at'); - spyOn(authStateService, 'getIdToken').and.returnValue('idt'); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - - checkAuthService - .checkAuth(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: true, - userData: undefined, - accessToken: 'at', - configId: 'configId1', - idToken: 'idt', - }); - }); - })); - - it('if authenticated set auth and fires event ', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - - const spy = spyOn(authStateService, 'setAuthenticatedAndFireEvent'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalled(); - }); - })); - - it('if authenticated publishUserdataIfExists', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - - const spy = spyOn(userService, 'publishUserDataIfExists'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalled(); - }); - })); - - it('if authenticated callbackService startTokenValidationPeriodically', waitForAsync(() => { - const config = { - authority: 'authority', - tokenRefreshInSeconds: 7, - }; - const allConfigs = [config]; - - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - const spy = spyOn( - periodicallyTokenCheckService, - 'startTokenValidationPeriodically' - ); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalled(); - }); - })); - - it('if isCheckSessionConfigured call checkSessionService.start()', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(checkSessionService, 'isCheckSessionConfigured').and.returnValue( - true - ); - const spy = spyOn(checkSessionService, 'start'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalled(); - }); - })); - - it('if isSilentRenewConfigured call getOrCreateIframe()', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( - true - ); - const spy = spyOn(silentRenewService, 'getOrCreateIframe'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalled(); - }); - })); - - it('calls checkSavedRedirectRouteAndNavigate if authenticated', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - const spy = spyOn(autoLoginService, 'checkSavedRedirectRouteAndNavigate'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledOnceWith(allConfigs[0]); - }); - })); - - it('does not call checkSavedRedirectRouteAndNavigate if not authenticated', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - const spy = spyOn(autoLoginService, 'checkSavedRedirectRouteAndNavigate'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(spy).toHaveBeenCalledTimes(0); - }); - })); - - it('fires CheckingAuth-Event on start and finished event on end', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - - const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(fireEventSpy.calls.allArgs()).toEqual([ - [EventTypes.CheckingAuth], - [EventTypes.CheckingAuthFinished], - ]); - }); - })); - - it('fires CheckingAuth-Event on start and CheckingAuthFinishedWithError event on end if exception occurs', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); - - spyOn(callBackService, 'isCallback').and.returnValue(true); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - throwError(() => new Error('ERROR')) - ); - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(fireEventSpy.calls.allArgs()).toEqual([ - [EventTypes.CheckingAuth], - [EventTypes.CheckingAuthFinishedWithError, 'ERROR'], - ]); - }); - })); - - it('fires CheckingAuth-Event on start and finished event on end if not authenticated', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( - 'http://localhost:4200' - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - - const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); - - checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { - expect(fireEventSpy.calls.allArgs()).toEqual([ - [EventTypes.CheckingAuth], - [EventTypes.CheckingAuthFinished], - ]); - }); - })); - }); - - describe('checkAuthIncludingServer', () => { - it('if isSilentRenewConfigured call getOrCreateIframe()', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( - of({ isAuthenticated: true } as LoginResponse) - ); - - spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( - true - ); - const spy = spyOn(silentRenewService, 'getOrCreateIframe'); - - checkAuthService - .checkAuthIncludingServer(allConfigs[0], allConfigs) - .subscribe(() => { - expect(spy).toHaveBeenCalled(); - }); - })); - - it('does forceRefreshSession get called and is NOT authenticated', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - - spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( - of({ - idToken: 'idToken', - accessToken: 'access_token', - isAuthenticated: false, - userData: null, - configId: 'configId1', - }) - ); - - checkAuthService - .checkAuthIncludingServer(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toBeTruthy(); - }); - })); - - it('should start check session and validation after forceRefreshSession has been called and is authenticated after forcing with silentrenew', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(checkSessionService, 'isCheckSessionConfigured').and.returnValue( - true - ); - spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( - true - ); - - const checkSessionServiceStartSpy = spyOn(checkSessionService, 'start'); - const periodicallyTokenCheckServiceSpy = spyOn( - periodicallyTokenCheckService, - 'startTokenValidationPeriodically' - ); - const getOrCreateIframeSpy = spyOn( - silentRenewService, - 'getOrCreateIframe' - ); - - spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( - of({ - idToken: 'idToken', - accessToken: 'access_token', - isAuthenticated: true, - userData: null, - configId: 'configId1', - }) - ); - - checkAuthService - .checkAuthIncludingServer(allConfigs[0], allConfigs) - .subscribe(() => { - expect(checkSessionServiceStartSpy).toHaveBeenCalledOnceWith( - allConfigs[0] - ); - expect(periodicallyTokenCheckServiceSpy).toHaveBeenCalledTimes(1); - expect(getOrCreateIframeSpy).toHaveBeenCalledOnceWith(allConfigs[0]); - }); - })); - - it('should start check session and validation after forceRefreshSession has been called and is authenticated after forcing without silentrenew', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority' }, - ]; - - spyOn(callBackService, 'isCallback').and.returnValue(false); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( - of({} as CallbackContext) - ); - spyOn(checkSessionService, 'isCheckSessionConfigured').and.returnValue( - true - ); - spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( - false - ); - - const checkSessionServiceStartSpy = spyOn(checkSessionService, 'start'); - const periodicallyTokenCheckServiceSpy = spyOn( - periodicallyTokenCheckService, - 'startTokenValidationPeriodically' - ); - const getOrCreateIframeSpy = spyOn( - silentRenewService, - 'getOrCreateIframe' - ); - - spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( - of({ - idToken: 'idToken', - accessToken: 'access_token', - isAuthenticated: true, - userData: null, - configId: 'configId1', - }) - ); - - checkAuthService - .checkAuthIncludingServer(allConfigs[0], allConfigs) - .subscribe(() => { - expect(checkSessionServiceStartSpy).toHaveBeenCalledOnceWith( - allConfigs[0] - ); - expect(periodicallyTokenCheckServiceSpy).toHaveBeenCalledTimes(1); - expect(getOrCreateIframeSpy).not.toHaveBeenCalled(); - }); - })); - }); - - describe('checkAuthMultiple', () => { - it('uses config with matching state when url has state param and config with state param is stored', waitForAsync(() => { - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority1' }, - { configId: 'configId2', authority: 'some-authority2' }, - ]; - - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - 'the-state-param' - ); - spyOn(storagePersistenceService, 'read') - .withArgs('authStateControl', allConfigs[0]) - .and.returnValue('the-state-param'); - const spy = spyOn( - checkAuthService as any, - 'checkAuthWithConfig' - ).and.callThrough(); - - checkAuthService.checkAuthMultiple(allConfigs).subscribe((result) => { - expect(Array.isArray(result)).toBe(true); - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.calls.argsFor(0)).toEqual([ - allConfigs[0], - allConfigs, - undefined, - ]); - expect(spy.calls.argsFor(1)).toEqual([ - allConfigs[1], - allConfigs, - undefined, - ]); - }); - })); - - it('uses config from passed configId if configId was passed and returns all results', waitForAsync(() => { - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - null - ); - - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority1' }, - { configId: 'configId2', authority: 'some-authority2' }, - ]; const spy = spyOn( - checkAuthService as any, - 'checkAuthWithConfig' - ).and.callThrough(); - - checkAuthService.checkAuthMultiple(allConfigs).subscribe((result) => { - expect(Array.isArray(result)).toBe(true); - expect(spy.calls.allArgs()).toEqual([ - [ - { configId: 'configId1', authority: 'some-authority1' }, - allConfigs, - undefined, - ], - [ - { configId: 'configId2', authority: 'some-authority2' }, - allConfigs, - undefined, - ], - ]); - }); - })); - - it('runs through all configs if no parameter is passed and has no state in url', waitForAsync(() => { - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - null - ); - - const allConfigs = [ - { configId: 'configId1', authority: 'some-authority1' }, - { configId: 'configId2', authority: 'some-authority2' }, - ]; const spy = spyOn( - checkAuthService as any, - 'checkAuthWithConfig' - ).and.callThrough(); - - checkAuthService.checkAuthMultiple(allConfigs).subscribe((result) => { - expect(Array.isArray(result)).toBe(true); - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.calls.argsFor(0)).toEqual([ - { configId: 'configId1', authority: 'some-authority1' }, - allConfigs, - undefined, - ]); - expect(spy.calls.argsFor(1)).toEqual([ - { configId: 'configId2', authority: 'some-authority2' }, - allConfigs, - undefined, - ]); - }); - })); - - it('throws error if url has state param but no config could be found', waitForAsync(() => { - spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( - 'the-state-param' - ); - - const allConfigs: OpenIdConfiguration[] = []; - - checkAuthService.checkAuthMultiple(allConfigs).subscribe({ - error: (error) => { - expect(error.message).toEqual( - 'could not find matching config for state the-state-param' - ); - }, - }); - })); - }); -}); +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of, throwError } from 'rxjs'; +import { mockAbstractProvider, mockProvider } from '../../test/auto-mock'; +import { AutoLoginService } from '../auto-login/auto-login.service'; +import { CallbackService } from '../callback/callback.service'; +import { PeriodicallyTokenCheckService } from '../callback/periodically-token-check.service'; +import { RefreshSessionService } from '../callback/refresh-session.service'; +import { + StsConfigLoader, + StsConfigStaticLoader, +} from '../config/loader/config-loader'; +import { OpenIdConfiguration } from '../config/openid-configuration'; +import { CallbackContext } from '../flows/callback-context'; +import { FlowsDataService } from '../flows/flows-data.service'; +import { CheckSessionService } from '../iframe/check-session.service'; +import { SilentRenewService } from '../iframe/silent-renew.service'; +import { LoggerService } from '../logging/logger.service'; +import { LoginResponse } from '../login/login-response'; +import { PopUpService } from '../login/popup/popup.service'; +import { EventTypes } from '../public-events/event-types'; +import { PublicEventsService } from '../public-events/public-events.service'; +import { StoragePersistenceService } from '../storage/storage-persistence.service'; +import { UserService } from '../user-data/user.service'; +import { CurrentUrlService } from '../utils/url/current-url.service'; +import { AuthStateService } from './auth-state.service'; +import { CheckAuthService } from './check-auth.service'; + +describe('CheckAuthService', () => { + let checkAuthService: CheckAuthService; + let authStateService: AuthStateService; + let userService: UserService; + let checkSessionService: CheckSessionService; + let callBackService: CallbackService; + let silentRenewService: SilentRenewService; + let periodicallyTokenCheckService: PeriodicallyTokenCheckService; + let refreshSessionService: RefreshSessionService; + let popUpService: PopUpService; + let autoLoginService: AutoLoginService; + let storagePersistenceService: StoragePersistenceService; + let currentUrlService: CurrentUrlService; + let publicEventsService: PublicEventsService; + let flowsDataService: FlowsDataService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + providers: [ + mockProvider(CheckSessionService), + mockProvider(SilentRenewService), + mockProvider(UserService), + mockProvider(LoggerService), + mockProvider(AuthStateService), + mockProvider(CallbackService), + mockProvider(RefreshSessionService), + mockProvider(PeriodicallyTokenCheckService), + mockProvider(PopUpService), + mockProvider(CurrentUrlService), + mockProvider(PublicEventsService), + mockAbstractProvider(StsConfigLoader, StsConfigStaticLoader), + AutoLoginService, + mockProvider(StoragePersistenceService), + mockProvider(FlowsDataService), + ], + }); + }); + + beforeEach(() => { + checkAuthService = TestBed.inject(CheckAuthService); + refreshSessionService = TestBed.inject(RefreshSessionService); + userService = TestBed.inject(UserService); + authStateService = TestBed.inject(AuthStateService); + checkSessionService = TestBed.inject(CheckSessionService); + callBackService = TestBed.inject(CallbackService); + silentRenewService = TestBed.inject(SilentRenewService); + periodicallyTokenCheckService = TestBed.inject( + PeriodicallyTokenCheckService + ); + popUpService = TestBed.inject(PopUpService); + autoLoginService = TestBed.inject(AutoLoginService); + storagePersistenceService = TestBed.inject(StoragePersistenceService); + currentUrlService = TestBed.inject(CurrentUrlService); + publicEventsService = TestBed.inject(PublicEventsService); + flowsDataService = TestBed.inject(FlowsDataService); + }); + + afterEach(() => { + storagePersistenceService.clear({} as OpenIdConfiguration); + }); + + it('should create', () => { + expect(checkAuthService).toBeTruthy(); + }); + + describe('checkAuth', () => { + it('uses config with matching state when url has state param and config with state param is stored', waitForAsync(() => { + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + 'the-state-param' + ); + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(storagePersistenceService, 'read') + .withArgs('authStateControl', allConfigs[0]) + .and.returnValue('the-state-param'); + const spy = spyOn( + checkAuthService as any, + 'checkAuthWithConfig' + ).and.callThrough(); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalledOnceWith( + allConfigs[0], + allConfigs, + undefined + ); + }); + })); + + it('throws error when url has state param and stored config with matching state param is not found', waitForAsync(() => { + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + 'the-state-param' + ); + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(storagePersistenceService, 'read') + .withArgs('authStateControl', allConfigs[0]) + .and.returnValue('not-matching-state-param'); + const spy = spyOn( + checkAuthService as any, + 'checkAuthWithConfig' + ).and.callThrough(); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe({ + error: (err) => { + expect(err).toBeTruthy(); + expect(spy).not.toHaveBeenCalled(); + }, + }); + })); + + it('uses first/default config when no param is passed', waitForAsync(() => { + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + null + ); + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + const spy = spyOn( + checkAuthService as any, + 'checkAuthWithConfig' + ).and.callThrough(); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalledOnceWith( + { configId: 'configId1', authority: 'some-authority' }, + allConfigs, + undefined + ); + }); + })); + + it('returns null and sendMessageToMainWindow if currently in a popup', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(popUpService as any, 'canAccessSessionStorage').and.returnValue( + true + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOnProperty(popUpService as any, 'windowInternal').and.returnValue({ + opener: {} as Window, + }); + spyOn(storagePersistenceService, 'read').and.returnValue(null); + + spyOn(popUpService, 'isCurrentlyInPopup').and.returnValue(true); + const popupSpy = spyOn(popUpService, 'sendMessageToMainWindow'); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: false, + errorMessage: '', + userData: null, + idToken: '', + accessToken: '', + configId: '', + }); + expect(popupSpy).toHaveBeenCalled(); + }); + })); + + it('returns isAuthenticated: false with error message in case handleCallbackAndFireEvents throws an error', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(true); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + + const spy = spyOn( + callBackService, + 'handleCallbackAndFireEvents' + ).and.returnValue(throwError(() => new Error('ERROR'))); + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: false, + errorMessage: 'ERROR', + configId: 'configId1', + idToken: '', + userData: null, + accessToken: '', + }); + expect(spy).toHaveBeenCalled(); + }); + })); + + it('calls callbackService.handlePossibleStsCallback with current url when callback is true', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(true); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'getAccessToken').and.returnValue('at'); + spyOn(authStateService, 'getIdToken').and.returnValue('idt'); + + const spy = spyOn( + callBackService, + 'handleCallbackAndFireEvents' + ).and.returnValue(of({} as CallbackContext)); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: true, + userData: undefined, + accessToken: 'at', + configId: 'configId1', + idToken: 'idt', + }); + expect(spy).toHaveBeenCalled(); + }); + })); + + it('does NOT call handleCallbackAndFireEvents with current url when callback is false', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + + const spy = spyOn( + callBackService, + 'handleCallbackAndFireEvents' + ).and.returnValue(of({} as CallbackContext)); + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'getAccessToken').and.returnValue('at'); + spyOn(authStateService, 'getIdToken').and.returnValue('idt'); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: true, + userData: undefined, + accessToken: 'at', + configId: 'configId1', + idToken: 'idt', + }); + expect(spy).not.toHaveBeenCalled(); + }); + })); + + it('does fire the auth and user data events when it is not a callback from the security token service and is authenticated', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(userService, 'getUserDataFromStore').and.returnValue({ + some: 'user-data', + }); + spyOn(authStateService, 'getAccessToken').and.returnValue('at'); + spyOn(authStateService, 'getIdToken').and.returnValue('idt'); + + const setAuthorizedAndFireEventSpy = spyOn( + authStateService, + 'setAuthenticatedAndFireEvent' + ); + const userServiceSpy = spyOn(userService, 'publishUserDataIfExists'); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: true, + userData: { + some: 'user-data', + }, + accessToken: 'at', + configId: 'configId1', + idToken: 'idt', + }); + expect(setAuthorizedAndFireEventSpy).toHaveBeenCalled(); + expect(userServiceSpy).toHaveBeenCalled(); + }); + })); + + it('does NOT fire the auth and user data events when it is not a callback from the security token service and is NOT authenticated', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + spyOn(authStateService, 'getAccessToken').and.returnValue('at'); + spyOn(authStateService, 'getIdToken').and.returnValue('it'); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + + const setAuthorizedAndFireEventSpy = spyOn( + authStateService, + 'setAuthenticatedAndFireEvent' + ); + const userServiceSpy = spyOn(userService, 'publishUserDataIfExists'); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: false, + userData: undefined, + accessToken: 'at', + configId: 'configId1', + idToken: 'it', + }); + expect(setAuthorizedAndFireEventSpy).not.toHaveBeenCalled(); + expect(userServiceSpy).not.toHaveBeenCalled(); + }); + })); + + it('if authenticated return true', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'getAccessToken').and.returnValue('at'); + spyOn(authStateService, 'getIdToken').and.returnValue('idt'); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + + checkAuthService + .checkAuth(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: true, + userData: undefined, + accessToken: 'at', + configId: 'configId1', + idToken: 'idt', + }); + }); + })); + + it('if authenticated set auth and fires event ', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + + const spy = spyOn(authStateService, 'setAuthenticatedAndFireEvent'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalled(); + }); + })); + + it('if authenticated publishUserdataIfExists', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + + const spy = spyOn(userService, 'publishUserDataIfExists'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalled(); + }); + })); + + it('if authenticated callbackService startTokenValidationPeriodically', waitForAsync(() => { + const config = { + authority: 'authority', + tokenRefreshInSeconds: 7, + }; + const allConfigs = [config]; + + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + const spy = spyOn( + periodicallyTokenCheckService, + 'startTokenValidationPeriodically' + ); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalled(); + }); + })); + + it('if isCheckSessionConfigured call checkSessionService.start()', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(checkSessionService, 'isCheckSessionConfigured').and.returnValue( + true + ); + const spy = spyOn(checkSessionService, 'start'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalled(); + }); + })); + + it('if isSilentRenewConfigured call getOrCreateIframe()', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( + true + ); + const spy = spyOn(silentRenewService, 'getOrCreateIframe'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalled(); + }); + })); + + it('calls checkSavedRedirectRouteAndNavigate if authenticated', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + const spy = spyOn(autoLoginService, 'checkSavedRedirectRouteAndNavigate'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledOnceWith(allConfigs[0]); + }); + })); + + it('does not call checkSavedRedirectRouteAndNavigate if not authenticated', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + const spy = spyOn(autoLoginService, 'checkSavedRedirectRouteAndNavigate'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(spy).toHaveBeenCalledTimes(0); + }); + })); + + it('fires CheckingAuth-Event on start and finished event on end', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + + const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(fireEventSpy.calls.allArgs()).toEqual([ + [EventTypes.CheckingAuth], + [EventTypes.CheckingAuthFinished], + ]); + }); + })); + + it('fires CheckingAuth-Event on start and CheckingAuthFinishedWithError event on end if exception occurs', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); + + spyOn(callBackService, 'isCallback').and.returnValue(true); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + throwError(() => new Error('ERROR')) + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(fireEventSpy.calls.allArgs()).toEqual([ + [EventTypes.CheckingAuth], + [EventTypes.CheckingAuthFinishedWithError, 'ERROR'], + ]); + }); + })); + + it('fires CheckingAuth-Event on start and finished event on end if not authenticated', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + + const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(fireEventSpy.calls.allArgs()).toEqual([ + [EventTypes.CheckingAuth], + [EventTypes.CheckingAuthFinished], + ]); + }); + })); + + it('resets a previously abandoned code flow when the current url is NOT a callback', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + const resetSpy = spyOn(flowsDataService, 'resetCodeFlowInProgress'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(resetSpy).toHaveBeenCalledOnceWith(allConfigs[0]); + }); + })); + + it('does NOT reset the code flow when the current url IS a callback', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(true); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(currentUrlService, 'getCurrentUrl').and.returnValue( + 'http://localhost:4200' + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + const resetSpy = spyOn(flowsDataService, 'resetCodeFlowInProgress'); + + checkAuthService.checkAuth(allConfigs[0], allConfigs).subscribe(() => { + expect(resetSpy).not.toHaveBeenCalled(); + }); + })); + }); + + describe('checkAuthIncludingServer', () => { + it('if isSilentRenewConfigured call getOrCreateIframe()', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( + of({ isAuthenticated: true } as LoginResponse) + ); + + spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( + true + ); + const spy = spyOn(silentRenewService, 'getOrCreateIframe'); + + checkAuthService + .checkAuthIncludingServer(allConfigs[0], allConfigs) + .subscribe(() => { + expect(spy).toHaveBeenCalled(); + }); + })); + + it('does forceRefreshSession get called and is NOT authenticated', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + + spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( + of({ + idToken: 'idToken', + accessToken: 'access_token', + isAuthenticated: false, + userData: null, + configId: 'configId1', + }) + ); + + checkAuthService + .checkAuthIncludingServer(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toBeTruthy(); + }); + })); + + it('should start check session and validation after forceRefreshSession has been called and is authenticated after forcing with silentrenew', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(checkSessionService, 'isCheckSessionConfigured').and.returnValue( + true + ); + spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( + true + ); + + const checkSessionServiceStartSpy = spyOn(checkSessionService, 'start'); + const periodicallyTokenCheckServiceSpy = spyOn( + periodicallyTokenCheckService, + 'startTokenValidationPeriodically' + ); + const getOrCreateIframeSpy = spyOn( + silentRenewService, + 'getOrCreateIframe' + ); + + spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( + of({ + idToken: 'idToken', + accessToken: 'access_token', + isAuthenticated: true, + userData: null, + configId: 'configId1', + }) + ); + + checkAuthService + .checkAuthIncludingServer(allConfigs[0], allConfigs) + .subscribe(() => { + expect(checkSessionServiceStartSpy).toHaveBeenCalledOnceWith( + allConfigs[0] + ); + expect(periodicallyTokenCheckServiceSpy).toHaveBeenCalledTimes(1); + expect(getOrCreateIframeSpy).toHaveBeenCalledOnceWith(allConfigs[0]); + }); + })); + + it('should start check session and validation after forceRefreshSession has been called and is authenticated after forcing without silentrenew', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority' }, + ]; + + spyOn(callBackService, 'isCallback').and.returnValue(false); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + spyOn(callBackService, 'handleCallbackAndFireEvents').and.returnValue( + of({} as CallbackContext) + ); + spyOn(checkSessionService, 'isCheckSessionConfigured').and.returnValue( + true + ); + spyOn(silentRenewService, 'isSilentRenewConfigured').and.returnValue( + false + ); + + const checkSessionServiceStartSpy = spyOn(checkSessionService, 'start'); + const periodicallyTokenCheckServiceSpy = spyOn( + periodicallyTokenCheckService, + 'startTokenValidationPeriodically' + ); + const getOrCreateIframeSpy = spyOn( + silentRenewService, + 'getOrCreateIframe' + ); + + spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( + of({ + idToken: 'idToken', + accessToken: 'access_token', + isAuthenticated: true, + userData: null, + configId: 'configId1', + }) + ); + + checkAuthService + .checkAuthIncludingServer(allConfigs[0], allConfigs) + .subscribe(() => { + expect(checkSessionServiceStartSpy).toHaveBeenCalledOnceWith( + allConfigs[0] + ); + expect(periodicallyTokenCheckServiceSpy).toHaveBeenCalledTimes(1); + expect(getOrCreateIframeSpy).not.toHaveBeenCalled(); + }); + })); + }); + + describe('checkAuthMultiple', () => { + it('uses config with matching state when url has state param and config with state param is stored', waitForAsync(() => { + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority1' }, + { configId: 'configId2', authority: 'some-authority2' }, + ]; + + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + 'the-state-param' + ); + spyOn(storagePersistenceService, 'read') + .withArgs('authStateControl', allConfigs[0]) + .and.returnValue('the-state-param'); + const spy = spyOn( + checkAuthService as any, + 'checkAuthWithConfig' + ).and.callThrough(); + + checkAuthService.checkAuthMultiple(allConfigs).subscribe((result) => { + expect(Array.isArray(result)).toBe(true); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.calls.argsFor(0)).toEqual([ + allConfigs[0], + allConfigs, + undefined, + ]); + expect(spy.calls.argsFor(1)).toEqual([ + allConfigs[1], + allConfigs, + undefined, + ]); + }); + })); + + it('uses config from passed configId if configId was passed and returns all results', waitForAsync(() => { + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + null + ); + + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority1' }, + { configId: 'configId2', authority: 'some-authority2' }, + ]; + const spy = spyOn( + checkAuthService as any, + 'checkAuthWithConfig' + ).and.callThrough(); + + checkAuthService.checkAuthMultiple(allConfigs).subscribe((result) => { + expect(Array.isArray(result)).toBe(true); + expect(spy.calls.allArgs()).toEqual([ + [ + { configId: 'configId1', authority: 'some-authority1' }, + allConfigs, + undefined, + ], + [ + { configId: 'configId2', authority: 'some-authority2' }, + allConfigs, + undefined, + ], + ]); + }); + })); + + it('runs through all configs if no parameter is passed and has no state in url', waitForAsync(() => { + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + null + ); + + const allConfigs = [ + { configId: 'configId1', authority: 'some-authority1' }, + { configId: 'configId2', authority: 'some-authority2' }, + ]; + const spy = spyOn( + checkAuthService as any, + 'checkAuthWithConfig' + ).and.callThrough(); + + checkAuthService.checkAuthMultiple(allConfigs).subscribe((result) => { + expect(Array.isArray(result)).toBe(true); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.calls.argsFor(0)).toEqual([ + { configId: 'configId1', authority: 'some-authority1' }, + allConfigs, + undefined, + ]); + expect(spy.calls.argsFor(1)).toEqual([ + { configId: 'configId2', authority: 'some-authority2' }, + allConfigs, + undefined, + ]); + }); + })); + + it('throws error if url has state param but no config could be found', waitForAsync(() => { + spyOn(currentUrlService, 'getStateParamFromCurrentUrl').and.returnValue( + 'the-state-param' + ); + + const allConfigs: OpenIdConfiguration[] = []; + + checkAuthService.checkAuthMultiple(allConfigs).subscribe({ + error: (error) => { + expect(error.message).toEqual( + 'could not find matching config for state the-state-param' + ); + }, + }); + })); + }); +}); diff --git a/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.ts b/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.ts index 6c9c7d060..fd0c8c04c 100644 --- a/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/auth-state/check-auth.service.ts @@ -6,6 +6,7 @@ import { CallbackService } from '../callback/callback.service'; import { PeriodicallyTokenCheckService } from '../callback/periodically-token-check.service'; import { RefreshSessionService } from '../callback/refresh-session.service'; import { OpenIdConfiguration } from '../config/openid-configuration'; +import { FlowsDataService } from '../flows/flows-data.service'; import { CheckSessionService } from '../iframe/check-session.service'; import { SilentRenewService } from '../iframe/silent-renew.service'; import { LoggerService } from '../logging/logger.service'; @@ -37,6 +38,7 @@ export class CheckAuthService { StoragePersistenceService ); private readonly publicEventsService = inject(PublicEventsService); + private readonly flowsDataService = inject(FlowsDataService); checkAuth( configuration: OpenIdConfiguration | null, @@ -215,6 +217,14 @@ export class CheckAuthService { const isCallback = this.callbackService.isCallback(currentUrl, config); + if (!isCallback) { + // No authorization callback present, so any previously started code flow + // was abandoned (e.g. the user returned to the app without the callback + // URL). Reset the flag so the periodic token check is not blocked + // indefinitely after authenticating via silent renew. See issue #2221. + this.flowsDataService.resetCodeFlowInProgress(config); + } + this.loggerService.logDebug( config, `currentUrl to check auth with: '${currentUrl}'`