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 34310d60..155de450 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,906 @@ -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 { 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', + oidcError: undefined, + 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' + ); + }, + }); + })); + }); +}); 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 6c9c7d06..b8d585d7 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 @@ -17,6 +17,7 @@ import { StoragePersistenceService } from '../storage/storage-persistence.servic import { UserService } from '../user-data/user.service'; import { CurrentUrlService } from '../utils/url/current-url.service'; import { AuthStateService } from './auth-state.service'; +import { OidcError } from '../flows/callback-handling/oidc-error'; @Injectable({ providedIn: 'root' }) export class CheckAuthService { @@ -263,7 +264,9 @@ export class CheckAuthService { this.autoLoginService.checkSavedRedirectRouteAndNavigate(config); } }), - catchError(({ message }) => { + catchError(error => { + const message = error.message; + this.loggerService.logError(config, message); this.publicEventsService.fireEvent( EventTypes.CheckingAuthFinishedWithError, @@ -273,6 +276,7 @@ export class CheckAuthService { const result: LoginResponse = { isAuthenticated: false, errorMessage: message, + oidcError: error instanceof OidcError ? error : undefined, userData: null, idToken: '', accessToken: '', diff --git a/projects/angular-auth-oidc-client/src/lib/callback/code-flow-callback.service.ts b/projects/angular-auth-oidc-client/src/lib/callback/code-flow-callback.service.ts index d93e4baf..0a3f6f2c 100644 --- a/projects/angular-auth-oidc-client/src/lib/callback/code-flow-callback.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/callback/code-flow-callback.service.ts @@ -7,6 +7,7 @@ import { CallbackContext } from '../flows/callback-context'; import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsService } from '../flows/flows.service'; import { IntervalService } from './interval.service'; +import { OidcError } from '../flows/callback-handling/oidc-error'; @Injectable({ providedIn: 'root' }) export class CodeFlowCallbackService { @@ -45,6 +46,10 @@ export class CodeFlowCallbackService { this.router.navigateByUrl(unauthorizedRoute); } + if (error instanceof OidcError) { + return throwError(() => error); + } + return throwError(() => new Error(error)); }) ); diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.spec.ts index d8a8108b..b8289e64 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.spec.ts @@ -11,6 +11,7 @@ import { TokenValidationService } from '../../validation/token-validation.servic import { CallbackContext } from '../callback-context'; import { FlowsDataService } from '../flows-data.service'; import { CodeFlowCallbackHandlerService } from './code-flow-callback-handler.service'; +import { OidcError } from './oidc-error'; describe('CodeFlowCallbackHandlerService', () => { let service: CodeFlowCallbackHandlerService; @@ -80,8 +81,32 @@ describe('CodeFlowCallbackHandlerService', () => { }); })); + it('throws oidc error if error parameter is returned in callback', waitForAsync(() => { + const getUrlParameterSpy = spyOn( + urlService, + 'getUrlParameter' + ).and.returnValue('params'); + + getUrlParameterSpy.withArgs('test-url', 'error').and.returnValue('FAILED_AUTH'); + getUrlParameterSpy.withArgs('test-url', 'error_description').and.returnValue('Failed auth'); + + service + .codeFlowCallback('test-url', { configId: 'configId1' }) + .subscribe({ + error: (err) => { + expect(err).toBeInstanceOf(OidcError); + }, + }); + })); + it('returns callbackContext if all params are good', waitForAsync(() => { - spyOn(urlService, 'getUrlParameter').and.returnValue('params'); + spyOn(urlService, 'getUrlParameter').and.callFake((urlToCheck: string, name: string) => { + if (name === 'error') { + return ''; + } + + return 'params'; + }); const expectedCallbackContext = { code: 'params', diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.ts index 38c0e229..7e759188 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/code-flow-callback-handler.service.ts @@ -11,6 +11,7 @@ import { TokenValidationService } from '../../validation/token-validation.servic import { AuthResult, CallbackContext } from '../callback-context'; import { FlowsDataService } from '../flows-data.service'; import { isNetworkError } from './error-helper'; +import { OidcError } from './oidc-error'; @Injectable({ providedIn: 'root' }) export class CodeFlowCallbackHandlerService { @@ -30,6 +31,9 @@ export class CodeFlowCallbackHandlerService { ): Observable { const code = this.urlService.getUrlParameter(urlToCheck, 'code'); const state = this.urlService.getUrlParameter(urlToCheck, 'state'); + const error = this.urlService.getUrlParameter(urlToCheck, 'error'); + const errorDescription = this.urlService.getUrlParameter(urlToCheck, 'error_description'); + const errorUri = this.urlService.getUrlParameter(urlToCheck, 'error_uri'); const sessionState = this.urlService.getUrlParameter( urlToCheck, 'session_state' @@ -41,6 +45,12 @@ export class CodeFlowCallbackHandlerService { return throwError(() => new Error('no state in url')); } + if (error) { + this.loggerService.logDebug(config, 'error in callback', error); + + return throwError(() => new OidcError(error, errorDescription, errorUri)); + } + if (!code) { this.loggerService.logDebug(config, 'no code in url'); diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/oidc-error.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/oidc-error.ts new file mode 100644 index 00000000..846a66aa --- /dev/null +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/oidc-error.ts @@ -0,0 +1,18 @@ + export class OidcError extends Error { + error: string; + + errorDescription?: string; + + errorUri?: string; + + constructor(error: string, errorDescription?: string, errorUri?: string) { + super(errorDescription || error); // Pass the error description or error to the base Error class + this.name = 'OidcError'; + this.error = error; + this.errorDescription = errorDescription; + this.errorUri = errorUri; + + // Set the prototype explicitly to retain the instance type in JavaScript + Object.setPrototypeOf(this, OidcError.prototype); + } + } diff --git a/projects/angular-auth-oidc-client/src/lib/login/login-response.ts b/projects/angular-auth-oidc-client/src/lib/login/login-response.ts index 09515c18..510d26bc 100644 --- a/projects/angular-auth-oidc-client/src/lib/login/login-response.ts +++ b/projects/angular-auth-oidc-client/src/lib/login/login-response.ts @@ -1,3 +1,5 @@ +import { OidcError } from '../flows/callback-handling/oidc-error'; + export interface LoginResponse { isAuthenticated: boolean; userData: any; @@ -5,4 +7,5 @@ export interface LoginResponse { idToken: string; configId?: string; errorMessage?: string; + oidcError?: OidcError; }