From 38fbe4565c586287fb0b6dbe3e73bacf7fc8c001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-=C3=89tienne=20Lord?= <7397743+pelord@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:34:59 -0500 Subject: [PATCH] chore(auth): jwt-decode from v2 to v4 (#1527) * chore(auth): jwt-decode from v2 to v4 * chore(auth/core): add an interface for jwt token handling * wip * simplification * chore(auth): decoding token could return null * chore(auth): substitute as with satisfies * chore(core): extends BaseUser interface to avoid circular dependencies * chore(core): analytics set user, optional user value to mandatory * simplification * wip * chore(dependency): remove jwt-decode as commonjs * chore(auth): handle null return form token decoding * wip --- angular.json | 1 - package-lock.json | 13 ++++++++----- package.json | 2 +- packages/auth/package.json | 2 +- packages/auth/src/lib/shared/admin.guard.ts | 3 +-- packages/auth/src/lib/shared/auth.interceptor.ts | 9 ++------- packages/auth/src/lib/shared/auth.interface.ts | 5 +++-- packages/auth/src/lib/shared/auth.service.ts | 13 +++++++------ packages/auth/src/lib/shared/index.ts | 1 + packages/auth/src/lib/shared/token.interface.ts | 7 +++++++ packages/auth/src/lib/shared/token.service.ts | 7 ++++--- packages/auth/src/public_api.ts | 1 + .../user-button/user-dialog.component.ts | 2 +- .../share-map/share-map-api.component.ts | 2 +- .../lib/analytics/shared/analytics.interface.ts | 6 ++++++ .../src/lib/analytics/shared/analytics.service.ts | 15 +++++---------- .../lib/analytics/analytics-listener.service.ts | 6 +++--- 17 files changed, 52 insertions(+), 43 deletions(-) create mode 100644 packages/auth/src/lib/shared/token.interface.ts diff --git a/angular.json b/angular.json index ba82d263b3..df6e722c6c 100644 --- a/angular.json +++ b/angular.json @@ -67,7 +67,6 @@ "jspdf", "jspdf-autotable", "jszip", - "jwt-decode", "moment", "nosleep.js", "raf", diff --git a/package-lock.json b/package-lock.json index 915d287b19..7c50d7982c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "jspdf": "^2.5.1", "jspdf-autotable": "^3.5.29", "jszip": "^3.10.1", - "jwt-decode": "^2.2.0", + "jwt-decode": "^4.0.0", "moment": "^2.29.4", "ngx-color": "^9.0.0", "ngx-indexed-db": "^11.0.2", @@ -15420,9 +15420,12 @@ } }, "node_modules/jwt-decode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", - "integrity": "sha512-86GgN2vzfUu7m9Wcj63iUkuDzFNYFVmjeDm2GzWpUk+opB0pEpMsw6ePCMrhYkumz2C1ihqtZzOMAg7FiXcNoQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } }, "node_modules/karma": { "version": "6.4.2", @@ -24912,7 +24915,7 @@ "version": "16.0.2", "license": "MIT", "dependencies": { - "jwt-decode": "^2.2.0", + "jwt-decode": "^4.0.0", "ts-cacheable": "^1.0.6", "ts-md5": "^1.3.0", "tslib": "^2.6.0" diff --git a/package.json b/package.json index 2888092d83..a6dc0dce66 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "jspdf": "^2.5.1", "jspdf-autotable": "^3.5.29", "jszip": "^3.10.1", - "jwt-decode": "^2.2.0", + "jwt-decode": "^4.0.0", "moment": "^2.29.4", "ngx-color": "^9.0.0", "ngx-indexed-db": "^11.0.2", diff --git a/packages/auth/package.json b/packages/auth/package.json index cf8e122813..390528ae03 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -28,7 +28,7 @@ } }, "dependencies": { - "jwt-decode": "^2.2.0", + "jwt-decode": "^4.0.0", "ts-cacheable": "^1.0.6", "ts-md5": "^1.3.0", "tslib": "^2.6.0" diff --git a/packages/auth/src/lib/shared/admin.guard.ts b/packages/auth/src/lib/shared/admin.guard.ts index ebc12c02e4..dcad5f0eb9 100644 --- a/packages/auth/src/lib/shared/admin.guard.ts +++ b/packages/auth/src/lib/shared/admin.guard.ts @@ -21,8 +21,7 @@ export class AdminGuard { ) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - const token = this.authService.decodeToken(); - if (token && token.user && token.user.isAdmin) { + if (this.authService.isAdmin) { return true; } diff --git a/packages/auth/src/lib/shared/auth.interceptor.ts b/packages/auth/src/lib/shared/auth.interceptor.ts index 0b17df25c6..3968041403 100644 --- a/packages/auth/src/lib/shared/auth.interceptor.ts +++ b/packages/auth/src/lib/shared/auth.interceptor.ts @@ -78,13 +78,8 @@ export class AuthInterceptor implements HttpInterceptor { headers: req.headers.set('Authorization', authHeader) }); - const tokenDecoded: any = this.tokenService.decode(); - if ( - authReq.params.get('_i') === 'true' && - tokenDecoded && - tokenDecoded.user && - tokenDecoded.user.sourceId - ) { + const tokenDecoded = this.tokenService.decode(); + if (authReq.params.get('_i') === 'true' && tokenDecoded?.user?.sourceId) { const hashUser = Md5.hashStr(tokenDecoded.user.sourceId) as string; authReq = authReq.clone({ params: authReq.params.set('_i', hashUser) diff --git a/packages/auth/src/lib/shared/auth.interface.ts b/packages/auth/src/lib/shared/auth.interface.ts index 5c4cfa3f81..c293a78a06 100644 --- a/packages/auth/src/lib/shared/auth.interface.ts +++ b/packages/auth/src/lib/shared/auth.interface.ts @@ -1,5 +1,6 @@ -import { MsalGuardConfiguration } from '@azure/msal-angular'; import { BaseUser } from '@igo2/core'; + +import { MsalGuardConfiguration } from '@azure/msal-angular'; import { BrowserAuthOptions } from '@azure/msal-browser'; export interface AuthInternOptions { @@ -102,7 +103,7 @@ export interface User extends BaseUser { sourceId?: string; locale?: string; isExpired?: boolean; - admin?: boolean; + isAdmin?: boolean; defaultContextId?: string; } diff --git a/packages/auth/src/lib/shared/auth.service.ts b/packages/auth/src/lib/shared/auth.service.ts index f78247ec52..940e009bfe 100644 --- a/packages/auth/src/lib/shared/auth.service.ts +++ b/packages/auth/src/lib/shared/auth.service.ts @@ -10,6 +10,7 @@ import { catchError, tap } from 'rxjs/operators'; import { globalCacheBusterNotifier } from 'ts-cacheable'; import { AuthOptions, IInfosUser, User } from './auth.interface'; +import { IgoJwtPayload } from './token.interface'; import { TokenService } from './token.service'; @Injectable({ @@ -28,8 +29,8 @@ export class AuthService { } get user(): User | null { - const { user = null } = this.decodeToken(); - return user; + const decodedToken = this.decodeToken(); + return decodedToken?.user ? decodedToken.user : null; } constructor( @@ -108,11 +109,11 @@ export class AuthService { return this.tokenService.get(); } - decodeToken() { + decodeToken(): IgoJwtPayload | null { if (this.isAuthenticated()) { return this.tokenService.decode(); } - return false; + return; } goToRedirectUrl() { @@ -163,7 +164,7 @@ export class AuthService { get isAdmin(): boolean { const token = this.decodeToken(); - if (token && token.user && token.user.isAdmin) { + if (token?.user?.isAdmin) { return true; } return false; @@ -176,7 +177,7 @@ export class AuthService { tap((data: any) => { this.tokenService.set(data.token); const tokenDecoded = this.decodeToken(); - if (tokenDecoded && tokenDecoded.user) { + if (tokenDecoded?.user) { if (tokenDecoded.user.locale && !this.languageForce) { this.languageService.setLanguage(tokenDecoded.user.locale); } diff --git a/packages/auth/src/lib/shared/index.ts b/packages/auth/src/lib/shared/index.ts index 3659fee2a4..ecc96bb2e9 100644 --- a/packages/auth/src/lib/shared/index.ts +++ b/packages/auth/src/lib/shared/index.ts @@ -1,4 +1,5 @@ export * from './token.service'; +export * from './token.interface'; export * from './auth.service'; export * from './auth.interface'; export * from './auth.interceptor'; diff --git a/packages/auth/src/lib/shared/token.interface.ts b/packages/auth/src/lib/shared/token.interface.ts new file mode 100644 index 0000000000..ead0bd1ba4 --- /dev/null +++ b/packages/auth/src/lib/shared/token.interface.ts @@ -0,0 +1,7 @@ +import { JwtPayload } from 'jwt-decode'; + +import { User } from './auth.interface'; + +export interface IgoJwtPayload extends JwtPayload { + user: User; +} diff --git a/packages/auth/src/lib/shared/token.service.ts b/packages/auth/src/lib/shared/token.service.ts index 6b0c49fea3..40a97935b2 100644 --- a/packages/auth/src/lib/shared/token.service.ts +++ b/packages/auth/src/lib/shared/token.service.ts @@ -2,9 +2,10 @@ import { Injectable, Injector } from '@angular/core'; import { ConfigService } from '@igo2/core'; -import jwtDecode from 'jwt-decode'; +import { jwtDecode } from 'jwt-decode'; import { AuthOptions } from './auth.interface'; +import { IgoJwtPayload } from './token.interface'; @Injectable({ providedIn: 'root' @@ -31,12 +32,12 @@ export class TokenService { return localStorage.getItem(this.tokenKey); } - decode() { + decode(): IgoJwtPayload | null { const token = this.get(); if (!token) { return; } - return jwtDecode(token); + return jwtDecode(token) satisfies IgoJwtPayload; } isExpired() { diff --git a/packages/auth/src/public_api.ts b/packages/auth/src/public_api.ts index fa18dde52b..e0c3bc4cb2 100644 --- a/packages/auth/src/public_api.ts +++ b/packages/auth/src/public_api.ts @@ -13,6 +13,7 @@ export * from './lib/shared/auth.interceptor'; export * from './lib/shared/auth.interface'; export * from './lib/shared/auth-microsoft.provider'; export * from './lib/shared/protected.directive'; +export * from './lib/shared/token.interface'; export * from './lib/shared/token.service'; export * from './lib/shared/auth-storage.interface'; export * from './lib/shared/auth-storage.service'; diff --git a/packages/context/src/lib/context-map-button/user-button/user-dialog.component.ts b/packages/context/src/lib/context-map-button/user-button/user-dialog.component.ts index 77fd2fe5f2..db0da1d0fc 100644 --- a/packages/context/src/lib/context-map-button/user-button/user-dialog.component.ts +++ b/packages/context/src/lib/context-map-button/user-button/user-dialog.component.ts @@ -18,7 +18,7 @@ export class UserDialogComponent { private storageService: StorageService ) { const decodeToken = this.auth.decodeToken(); - this.user = decodeToken.user; + this.user = decodeToken?.user; this.exp = new Date(decodeToken.exp * 1000).toLocaleString(); } diff --git a/packages/context/src/lib/share-map/share-map/share-map-api.component.ts b/packages/context/src/lib/share-map/share-map/share-map-api.component.ts index 23db048396..6c96c59b97 100644 --- a/packages/context/src/lib/share-map/share-map/share-map-api.component.ts +++ b/packages/context/src/lib/share-map/share-map/share-map-api.component.ts @@ -33,7 +33,7 @@ export class ShareMapApiComponent implements OnInit { ngOnInit(): void { this.auth.authenticate$.subscribe((auth) => { const decodeToken = this.auth.decodeToken(); - this.userId = decodeToken.user ? decodeToken.user.id : undefined; + this.userId = decodeToken?.user?.id.toString(); this.buildForm(); }); } diff --git a/packages/core/src/lib/analytics/shared/analytics.interface.ts b/packages/core/src/lib/analytics/shared/analytics.interface.ts index 260571fc33..4a7ca1b77f 100644 --- a/packages/core/src/lib/analytics/shared/analytics.interface.ts +++ b/packages/core/src/lib/analytics/shared/analytics.interface.ts @@ -1,3 +1,5 @@ +import { BaseUser } from '../../user/user.interface'; + export type AnalyticsProvider = 'matomo'; export interface AnalyticsOptions { @@ -5,3 +7,7 @@ export interface AnalyticsOptions { url?: string; id?: string; } + +export interface AnalyticsBaseUser extends BaseUser { + sourceId?: string | number; +} diff --git a/packages/core/src/lib/analytics/shared/analytics.service.ts b/packages/core/src/lib/analytics/shared/analytics.service.ts index f8b6cf398f..92d0f32f84 100644 --- a/packages/core/src/lib/analytics/shared/analytics.service.ts +++ b/packages/core/src/lib/analytics/shared/analytics.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ConfigService } from '../../config/config.service'; -import { AnalyticsOptions } from './analytics.interface'; +import { AnalyticsBaseUser, AnalyticsOptions } from './analytics.interface'; @Injectable({ providedIn: 'root' @@ -45,15 +45,10 @@ export class AnalyticsService { })(); } - public setUser( - user?: { - id: number; - sourceId?: string; - firstName?: string; - lastName?: string; - }, - profils?: string[] - ) { + /** + * Pass `null` to unset the user. + */ + public setUser(user: AnalyticsBaseUser | null, profils?: string[]) { if (this.options.provider === 'matomo') { if (!user) { this.paq.push(['resetUserId']); diff --git a/packages/integration/src/lib/analytics/analytics-listener.service.ts b/packages/integration/src/lib/analytics/analytics-listener.service.ts index e1eb7b665f..716ed2e09b 100644 --- a/packages/integration/src/lib/analytics/analytics-listener.service.ts +++ b/packages/integration/src/lib/analytics/analytics-listener.service.ts @@ -48,15 +48,15 @@ export class AnalyticsListenerService { listenUser() { this.authService.authenticate$.subscribe(() => { - const tokenDecoded = this.authService.decodeToken() || {}; - if (tokenDecoded.user) { + const tokenDecoded = this.authService.decodeToken(); + if (tokenDecoded?.user) { this.authService .getProfils() .subscribe((profils) => this.analyticsService.setUser(tokenDecoded.user, profils.profils) ); } else { - this.analyticsService.setUser(); + this.analyticsService.setUser(null); } }); }