Skip to content
Snippets Groups Projects
Commit 531cb738 authored by Martin's avatar Martin
Browse files

OZG-6986 OZG-7509 adjust authentication service

parent f0fbb704
No related branches found
No related tags found
1 merge request!25Ozg 6986 adjust routing
...@@ -23,30 +23,39 @@ ...@@ -23,30 +23,39 @@
*/ */
import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils';
import { UserProfileResource } from '@alfa-client/user-profile-shared'; import { UserProfileResource } from '@alfa-client/user-profile-shared';
import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc';
import { Environment } from 'libs/environment-shared/src/lib/environment.model';
import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; import { Subject } from 'rxjs';
import { AuthenticationService } from './authentication.service';
import { createAuthConfig } from '../../test/authentication';
import { createEnvironment } from '../../../environment-shared/test/environment'; import { createEnvironment } from '../../../environment-shared/test/environment';
import { Environment } from 'libs/environment-shared/src/lib/environment.model'; import { createAuthConfig, createOAuthEvent } from '../../test/authentication';
import { AuthenticationService } from './authentication.service';
describe('AuthenticationService', () => { describe('AuthenticationService', () => {
let service: AuthenticationService; let service: AuthenticationService;
let oAuthService: Mock<OAuthService>; let oAuthService: Mock<OAuthService>;
let environmentConfig: Environment; let environmentConfig: Environment;
let eventsSubject: Subject<OAuthEvent>;
beforeEach(() => { beforeEach(() => {
eventsSubject = new Subject<OAuthEvent>();
oAuthService = <any>{ oAuthService = <any>{
...mock(OAuthService), ...mock(OAuthService),
loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()), loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()),
hasValidAccessToken: jest.fn(),
hasValidIdToken: jest.fn(),
}; };
Object.defineProperty(oAuthService, 'events', { get: () => eventsSubject });
environmentConfig = createEnvironment(); environmentConfig = createEnvironment();
service = new AuthenticationService(useFromMock(oAuthService), environmentConfig); service = new AuthenticationService(useFromMock(oAuthService), environmentConfig);
}); });
describe('login', () => { describe('login', () => {
beforeEach(() => { beforeEach(() => {
service.setCurrentUser = jest.fn(); service.buildEventPromise = jest.fn();
}); });
it('should configure service with authConfig', async () => { it('should configure service with authConfig', async () => {
...@@ -72,17 +81,228 @@ describe('AuthenticationService', () => { ...@@ -72,17 +81,228 @@ describe('AuthenticationService', () => {
expect(oAuthService.tokenValidationHandler).not.toBeNull(); expect(oAuthService.tokenValidationHandler).not.toBeNull();
}); });
it('should build event promise', async () => {
service.buildEventPromise = jest.fn().mockResolvedValue(() => Promise.resolve());
await service.login();
expect(service.buildEventPromise).toHaveBeenCalled();
});
it('should load discovery document and login', () => { it('should load discovery document and login', () => {
service.login(); service.login();
expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled(); expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled();
}); });
it('should set current user', async () => { it('should return eventPromise', async () => {
await service.login(); const promise: Promise<void> = Promise.resolve();
service.buildEventPromise = jest.fn().mockResolvedValue(promise);
const returnPromise: Promise<void> = service.login();
await expect(returnPromise).resolves.toBeUndefined();
});
});
describe('build event promise', () => {
const event: OAuthEvent = createOAuthEvent();
beforeEach(() => {
service.shouldProceed = jest.fn().mockReturnValue(true);
service.setCurrentUser = jest.fn();
service.unsubscribeEvents = jest.fn();
});
it('should call shouldProceed on event trigger', () => {
service.buildEventPromise();
eventsSubject.next(event);
expect(service.shouldProceed).toHaveBeenCalledWith(event);
});
describe('on next', () => {
it('should set current user', () => {
service.buildEventPromise();
eventsSubject.next(event);
expect(service.setCurrentUser).toHaveBeenCalled(); expect(service.setCurrentUser).toHaveBeenCalled();
}); });
it('should unsubscribe event', () => {
service.buildEventPromise();
eventsSubject.next(event);
expect(service.unsubscribeEvents).toHaveBeenCalled();
});
it('should resolved promise with a valid event', async () => {
const promise: Promise<void> = service.buildEventPromise();
eventsSubject.next(event);
await expect(promise).resolves.toBeUndefined();
});
});
describe('on error', () => {
const errorMessage: string = 'Test Error';
const error: Error = new Error(errorMessage);
it('should unsubscribe event', () => {
service.buildEventPromise();
eventsSubject.error(error);
expect(service.unsubscribeEvents).toHaveBeenCalled();
});
it('should reject the promise with an error', async () => {
const promise: Promise<void> = service.buildEventPromise();
eventsSubject.error(error);
await expect(promise).rejects.toThrow(errorMessage);
});
});
});
describe('should proceed', () => {
const event: OAuthEvent = createOAuthEvent();
it('should call considered as login event', () => {
service.consideredAsLoginEvent = jest.fn();
service.shouldProceed(event);
expect(service.consideredAsLoginEvent).toHaveBeenCalledWith(event.type);
});
it('should return true on login event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(true);
const proceed: boolean = service.shouldProceed(event);
expect(proceed).toBeTruthy();
});
it('should call considered as page reload event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(false);
service.consideredAsPageReloadEvent = jest.fn();
service.shouldProceed(event);
expect(service.consideredAsPageReloadEvent).toHaveBeenCalledWith(event.type);
});
it('should return true on page reload event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(false);
service.consideredAsPageReloadEvent = jest.fn().mockReturnValue(true);
const proceed: boolean = service.shouldProceed(event);
expect(proceed).toBeTruthy();
});
it('should return false on non login or page reload event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(false);
service.consideredAsPageReloadEvent = jest.fn().mockReturnValue(false);
const proceed: boolean = service.shouldProceed(event);
expect(proceed).toBeFalsy();
});
});
describe('consideredAsLoginEvent', () => {
it('should return true if event is "token_received"', () => {
const event: string = 'token_received';
const result: boolean = service.consideredAsLoginEvent(event);
expect(result).toBeTruthy();
});
it('should return false if event is not "token_received"', () => {
const event: string = 'something_else';
const result: boolean = service.consideredAsLoginEvent(event);
expect(result).toBeFalsy();
});
});
describe('consideredAsPageReloadEvent', () => {
it('should return true if event is "discovery_document_loaded" and tokens are valid', () => {
service.hasValidToken = jest.fn().mockReturnValue(true);
const event: string = 'discovery_document_loaded';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeTruthy();
});
it('should return false if event is "discovery_document_loaded" and tokens are invalid', () => {
service.hasValidToken = jest.fn().mockReturnValue(false);
const event: string = 'discovery_document_loaded';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeFalsy();
});
it('should return false if event is not "discovery_document_loaded" and tokens are valid', () => {
service.hasValidToken = jest.fn().mockReturnValue(true);
const event: string = 'something_else';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeFalsy();
});
it('should return false if event is not "discovery_document_loaded" and tokens are invalid', () => {
service.hasValidToken = jest.fn().mockReturnValue(false);
const event: string = 'something_else';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeFalsy();
});
});
describe('hasValidToken', () => {
it('should return true if both tokens are valid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(true);
oAuthService.hasValidIdToken.mockReturnValue(true);
const result: boolean = service.hasValidToken();
expect(result).toBeTruthy();
});
it('should return false if access token is invalid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(false);
const result: boolean = service.hasValidToken();
expect(result).toBeFalsy();
});
it('should return false if id token is invalid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(true);
oAuthService.hasValidIdToken.mockReturnValue(false);
const result: boolean = service.hasValidToken();
expect(result).toBeFalsy();
});
it('should return false if both tokens are invalid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(false);
oAuthService.hasValidIdToken.mockReturnValue(false);
const result: boolean = service.hasValidToken();
expect(result).toBeFalsy();
});
}); });
describe('set current user', () => { describe('set current user', () => {
......
...@@ -22,16 +22,20 @@ ...@@ -22,16 +22,20 @@
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared'; import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared';
import { isNotNull } from '@alfa-client/tech-shared';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model'; import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model';
import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util'; import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util';
import { filter, Subscription } from 'rxjs';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthenticationService { export class AuthenticationService {
currentUserResource: UserProfileResource; currentUserResource: UserProfileResource;
private eventSubscription: Subscription;
constructor( constructor(
private oAuthService: OAuthService, private oAuthService: OAuthService,
@Inject(ENVIRONMENT_CONFIG) private envConfig: Environment, @Inject(ENVIRONMENT_CONFIG) private envConfig: Environment,
...@@ -41,18 +45,48 @@ export class AuthenticationService { ...@@ -41,18 +45,48 @@ export class AuthenticationService {
this.oAuthService.configure(this.buildConfiguration()); this.oAuthService.configure(this.buildConfiguration());
this.oAuthService.setupAutomaticSilentRefresh(); this.oAuthService.setupAutomaticSilentRefresh();
this.oAuthService.tokenValidationHandler = new JwksValidationHandler(); this.oAuthService.tokenValidationHandler = new JwksValidationHandler();
const eventPromise: Promise<void> = this.buildEventPromise();
await this.oAuthService.loadDiscoveryDocumentAndLogin(); await this.oAuthService.loadDiscoveryDocumentAndLogin();
return eventPromise;
}
buildEventPromise(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.eventSubscription = this.oAuthService.events.pipe(filter((event: OAuthEvent) => this.shouldProceed(event))).subscribe({
next: () => {
this.setCurrentUser(); this.setCurrentUser();
this.unsubscribeEvents();
resolve();
},
error: (error: any) => {
this.unsubscribeEvents();
reject(error);
},
});
});
}
shouldProceed(event: OAuthEvent): boolean {
return this.consideredAsLoginEvent(event.type) || this.consideredAsPageReloadEvent(event.type);
}
consideredAsLoginEvent(eventType: string): boolean {
return eventType === 'token_received';
}
consideredAsPageReloadEvent(eventType: string): boolean {
return eventType === 'discovery_document_loaded' && this.hasValidToken();
}
hasValidToken(): boolean {
return this.oAuthService.hasValidAccessToken() && this.oAuthService.hasValidIdToken();
} }
buildConfiguration(): AuthConfig { buildConfiguration(): AuthConfig {
return { return {
issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm, issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm,
tokenEndpoint: tokenEndpoint: this.envConfig.authServer + '/realms/' + this.envConfig.realm + '/protocol/openid-connect/token',
this.envConfig.authServer +
'/realms/' +
this.envConfig.realm +
'/protocol/openid-connect/token',
redirectUri: window.location.origin + '/', redirectUri: window.location.origin + '/',
clientId: this.envConfig.clientId, clientId: this.envConfig.clientId,
scope: 'openid profile', scope: 'openid profile',
...@@ -71,10 +105,18 @@ export class AuthenticationService { ...@@ -71,10 +105,18 @@ export class AuthenticationService {
this.currentUserResource = userResource; this.currentUserResource = userResource;
} }
unsubscribeEvents(): void {
this.eventSubscription.unsubscribe();
}
public getCurrentUserInitials(): string { public getCurrentUserInitials(): string {
return getUserNameInitials(this.currentUserResource); return getUserNameInitials(this.currentUserResource);
} }
public isLoggedIn(): boolean {
return isNotNull(this.oAuthService.getIdentityClaims());
}
public logout(): void { public logout(): void {
this.oAuthService.revokeTokenAndLogout(); this.oAuthService.revokeTokenAndLogout();
} }
......
...@@ -21,8 +21,13 @@ ...@@ -21,8 +21,13 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
import { AuthConfig } from 'angular-oauth2-oidc'; import { faker } from '@faker-js/faker';
import { AuthConfig, OAuthEvent } from 'angular-oauth2-oidc';
export function createAuthConfig(): AuthConfig { export function createAuthConfig(): AuthConfig {
return {}; return {};
} }
export function createOAuthEvent(): OAuthEvent {
return { type: <any>faker.lorem.word() };
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment