From 9e96fed71eb49db0480cc27170e5e0814adcc314 Mon Sep 17 00:00:00 2001 From: Wojciech Duda <69160975+wojd0@users.noreply.github.com> Date: Wed, 9 Oct 2024 17:06:30 +0200 Subject: [PATCH] AAE-26321 Custom JWT storage service optional injection token (#10292) * AAE-26321 Custom JWT storage service optional injection token * AAE-26321 custom storage service test * AAE-26321 Use only the injection token with factory * AAE-26321 improve custom storage test --- .../auth/services/jwt-helper.service.spec.ts | 127 +++++++++++------- .../lib/auth/services/jwt-helper.service.ts | 22 +-- 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/lib/core/src/lib/auth/services/jwt-helper.service.spec.ts b/lib/core/src/lib/auth/services/jwt-helper.service.spec.ts index 0c56406fef..5a89c8b828 100644 --- a/lib/core/src/lib/auth/services/jwt-helper.service.spec.ts +++ b/lib/core/src/lib/auth/services/jwt-helper.service.spec.ts @@ -15,17 +15,34 @@ * limitations under the License. */ -import { JwtHelperService } from './jwt-helper.service'; +import { JWT_STORAGE_SERVICE, JwtHelperService } from './jwt-helper.service'; import { mockToken } from '../mock/jwt-helper.service.spec'; import { TestBed } from '@angular/core/testing'; +import { StorageService } from '../../common'; +import { OAuthStorage } from 'angular-oauth2-oidc'; + +const mockStorage = { + access_token: 'my-access_token', + id_token: 'my-id_token', + getItem(key: string) { + return this[key]; + } +}; + +const mockCustomStorage = { + access_token: 'my-custom-access_token', + id_token: 'my-custom-id_token', + getItem(key: string) { + return this[key]; + } +}; describe('JwtHelperService', () => { - let jwtHelperService: JwtHelperService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [JwtHelperService] + providers: [JwtHelperService, { provide: StorageService, useValue: mockStorage }] }); jwtHelperService = TestBed.inject(JwtHelperService); }); @@ -44,96 +61,104 @@ describe('JwtHelperService', () => { }); describe('RealmRole ', () => { - it('Should be true if the realm_access contains the single role', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - realm_access: { roles: ['role1'] } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + realm_access: { roles: ['role1'] } + }); const result = jwtHelperService.hasRealmRole('role1'); expect(result).toBeTruthy(); }); it('Should be true if the realm_access contains at least one of the roles', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - realm_access: { roles: ['role1'] } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + realm_access: { roles: ['role1'] } + }); const result = jwtHelperService.hasRealmRoles(['role1', 'role2']); expect(result).toBeTruthy(); }); it('Should be false if the realm_access does not contain the role', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - realm_access: { roles: ['role3'] } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + realm_access: { roles: ['role3'] } + }); const result = jwtHelperService.hasRealmRole('role1'); expect(result).toBeFalsy(); }); it('Should be false if the realm_access does not contain at least one of the roles', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - realm_access: { roles: ['role1'] } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + realm_access: { roles: ['role1'] } + }); const result = jwtHelperService.hasRealmRoles(['role3', 'role2']); expect(result).toBeFalsy(); }); - }); + }); describe('ClientRole ', () => { - it('Should be true if the resource_access contains the single role', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - resource_access: { fakeApp: { roles: ['role1'] } } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + resource_access: { fakeApp: { roles: ['role1'] } } + }); const result = jwtHelperService.hasRealmRolesForClientRole('fakeApp', ['role1']); expect(result).toBeTruthy(); }); it('Should be true if the resource_access contains at least one of the roles', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - resource_access: { fakeApp: { roles: ['role1'] } } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + resource_access: { fakeApp: { roles: ['role1'] } } + }); const result = jwtHelperService.hasRealmRolesForClientRole('fakeApp', ['role1', 'role2']); expect(result).toBeTruthy(); }); it('Should be false if the resource_access does not contain the role', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - resource_access: { fakeApp: { roles: ['role3'] } } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + resource_access: { fakeApp: { roles: ['role3'] } } + }); const result = jwtHelperService.hasRealmRolesForClientRole('fakeApp', ['role1', 'role2']); expect(result).toBeFalsy(); }); it('Should be false if the resource_access does not contain the client role related to the app', () => { - spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); - spyOn(jwtHelperService, 'decodeToken').and.returnValue( - { - resource_access: { anotherFakeApp: { roles: ['role1'] } } - }); + spyOn(jwtHelperService, 'decodeToken').and.returnValue({ + resource_access: { anotherFakeApp: { roles: ['role1'] } } + }); const result = jwtHelperService.hasRealmRolesForClientRole('fakeApp', ['role1', 'role2']); expect(result).toBeFalsy(); }); - }); + }); +}); + +describe('JwtHelperService with custom storage service', () => { + let jwtHelperService: JwtHelperService; + let defaultStorage: StorageService; + let customStorage: OAuthStorage; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + JwtHelperService, + { provide: StorageService, useValue: mockStorage }, + { provide: JWT_STORAGE_SERVICE, useValue: mockCustomStorage } + ] + }); + jwtHelperService = TestBed.inject(JwtHelperService); + defaultStorage = TestBed.inject(StorageService); + customStorage = TestBed.inject(JWT_STORAGE_SERVICE); + }); + + it('should use the custom storage service', () => { + const customStorageGetItemSpy = spyOn(customStorage, 'getItem').and.callThrough(); + const defaultStorageGetItemSpy = spyOn(defaultStorage, 'getItem').and.callThrough(); + const result = jwtHelperService.getIdToken(); + + expect(customStorage).toBeDefined(); + expect(customStorageGetItemSpy).toHaveBeenCalled(); + expect(defaultStorageGetItemSpy).not.toHaveBeenCalled(); + expect(result).toBe('my-custom-id_token'); + }); }); diff --git a/lib/core/src/lib/auth/services/jwt-helper.service.ts b/lib/core/src/lib/auth/services/jwt-helper.service.ts index d992f48278..bb4bf43f71 100644 --- a/lib/core/src/lib/auth/services/jwt-helper.service.ts +++ b/lib/core/src/lib/auth/services/jwt-helper.service.ts @@ -15,14 +15,19 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { inject, Injectable, InjectionToken } from '@angular/core'; +import { OAuthStorage } from 'angular-oauth2-oidc'; import { StorageService } from '../../common/services/storage.service'; +export const JWT_STORAGE_SERVICE = new InjectionToken('JWT_STORAGE_SERVICE', { + providedIn: 'root', + factory: () => inject(StorageService) +}); + @Injectable({ providedIn: 'root' }) export class JwtHelperService { - static USER_NAME = 'name'; static FAMILY_NAME = 'family_name'; static GIVEN_NAME = 'given_name'; @@ -34,8 +39,7 @@ export class JwtHelperService { static USER_PREFERRED_USERNAME = 'preferred_username'; static HXP_AUTHORIZATION = 'hxp_authorization'; - constructor(private storageService: StorageService) { - } + private storageService: OAuthStorage = inject(JWT_STORAGE_SERVICE); /** * Decodes a JSON web token into a JS object. @@ -85,7 +89,7 @@ export class JwtHelperService { * @param key Key name of the field to retrieve * @returns Value from the token */ - getValueFromLocalToken(key: string): T { + getValueFromLocalToken(key: string): T { return this.getValueFromToken(this.getAccessToken(), key) || this.getValueFromToken(this.getIdToken(), key); } @@ -114,7 +118,7 @@ export class JwtHelperService { * @param key Key name of the field to retrieve * @returns Value from the token */ - getValueFromLocalIdToken(key: string): T { + getValueFromLocalIdToken(key: string): T { return this.getValueFromToken(this.getIdToken(), key); } @@ -123,7 +127,7 @@ export class JwtHelperService { * * @returns id token */ - getIdToken(): string { + getIdToken(): string { return this.storageService.getItem(JwtHelperService.USER_ID_TOKEN); } @@ -186,7 +190,7 @@ export class JwtHelperService { * @param rolesToCheck List of role names to check * @returns True if it contains at least one of the given roles, false otherwise */ - hasRealmRoles(rolesToCheck: string []): boolean { + hasRealmRoles(rolesToCheck: string[]): boolean { return rolesToCheck.some((currentRole) => this.hasRealmRole(currentRole)); } @@ -197,7 +201,7 @@ export class JwtHelperService { * @param rolesToCheck List of role names to check * @returns True if it contains at least one of the given roles, false otherwise */ - hasRealmRolesForClientRole(clientName: string, rolesToCheck: string []): boolean { + hasRealmRolesForClientRole(clientName: string, rolesToCheck: string[]): boolean { return rolesToCheck.some((currentRole) => this.hasClientRole(clientName, currentRole)); }