From 62ea2db5a42d2886c5a37416883507ff64e94099 Mon Sep 17 00:00:00 2001 From: ms-emp Date: Fri, 15 Nov 2024 11:57:57 -0500 Subject: [PATCH] feat(refresh_token): retrieve/restore refresh_token This enables the use of biometric login --- projects/auth-js/oidc/models/args.model.ts | 2 +- projects/auth-js/oidc/oidc-auth-manager.ts | 35 ++++++++++++++++++---- projects/ngx-auth/core/auth.service.ts | 14 +++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/projects/auth-js/oidc/models/args.model.ts b/projects/auth-js/oidc/models/args.model.ts index b422adf8..0e2dcef0 100644 --- a/projects/auth-js/oidc/models/args.model.ts +++ b/projects/auth-js/oidc/models/args.model.ts @@ -13,7 +13,7 @@ export type LogoutArgs = MobileWindowParams & PopupWindowParams & RedirectParams desktopNavigationType?: DesktopNavigation; }; -export type RenewArgs = IFrameWindowParams & ExtraSigninRequestArgs; +export type RenewArgs = IFrameWindowParams & ExtraSigninRequestArgs & { refreshToken?: string }; export type SigninMobileArgs = MobileWindowParams & ExtraSigninRequestArgs; diff --git a/projects/auth-js/oidc/oidc-auth-manager.ts b/projects/auth-js/oidc/oidc-auth-manager.ts index 6d198dbd..5ba95afd 100644 --- a/projects/auth-js/oidc/oidc-auth-manager.ts +++ b/projects/auth-js/oidc/oidc-auth-manager.ts @@ -3,7 +3,7 @@ import { merge } from 'lodash-es'; import { - ErrorResponse, InMemoryWebStorage, Log, SigninSilentArgs, User, UserProfile, WebStorageStateStore + ErrorResponse, IdTokenClaims, InMemoryWebStorage, Log, SigninSilentArgs, User, UserProfile, WebStorageStateStore } from 'oidc-client-ts'; import { AuthManager, AuthSubscriber, AuthSubscription, AuthSubscriptions, AuthUtils, Optional } from '../core'; @@ -40,6 +40,7 @@ const DEFAULT_SETTINGS: Optional export class OIDCAuthManager extends AuthManager { #idTokenSubs = new AuthSubscriptions<[string | undefined]>(); #accessTokenSubs = new AuthSubscriptions<[string | undefined]>(); + #refreshTokenSubs = new AuthSubscriptions<[string | undefined]>(); #userProfileSubs = new AuthSubscriptions<[UserProfile | undefined]>(); #userSessionSubs = new AuthSubscriptions<[UserSession | undefined]>(); #authenticatedSubs = new AuthSubscriptions<[boolean]>(); @@ -49,6 +50,7 @@ export class OIDCAuthManager extends AuthManager { #idToken?: string; #accessToken?: string; + #refreshToken?: string; #userProfile?: UserProfile; #userSession?: UserSession; #isAuthenticated = false; @@ -62,14 +64,16 @@ export class OIDCAuthManager extends AuthManager { if (this.#user !== value) { this.#user = value; - this.#idToken = (value) ? value.id_token : undefined; - this.#accessToken = (value) ? value.access_token : undefined; - this.#userProfile = (value?.profile) ? value.profile : undefined; - this.#userSession = (value) ? UserSession.deserialize(value) : undefined; + this.#idToken = value ? value.id_token : undefined; + this.#accessToken = value ? value.access_token : undefined; + this.#refreshToken = value ? value.refresh_token : undefined; + this.#userProfile = value?.profile ? value.profile : undefined; + this.#userSession = value ? UserSession.deserialize(value) : undefined; this.#isAuthenticated = !!(value && !value.expired); this.#idTokenSubs.notify(this.#idToken); this.#accessTokenSubs.notify(this.#accessToken); + this.#refreshTokenSubs.notify(this.#refreshToken); this.#userProfileSubs.notify(this.#userProfile); this.#userSessionSubs.notify(this.#userSession); this.#authenticatedSubs.notify(this.#isAuthenticated); @@ -220,6 +224,17 @@ export class OIDCAuthManager extends AuthManager { } public async renew(args?: RenewArgs): Promise { + if (args?.refreshToken) { + await this.#userManager?.storeUser( + new User({ + refresh_token: args.refreshToken, + access_token: undefined as unknown as string, + profile: undefined as unknown as IdTokenClaims, + token_type: undefined as unknown as string + }) + ); + } + return this.#signinSilent(args).catch(error => console.error(error)); } @@ -266,11 +281,17 @@ export class OIDCAuthManager extends AuthManager { return AuthUtils.decodeJwt(this.#accessToken); } + public async getRefreshToken(): Promise { + await this.#waitForRenew('getRefreshToken()'); + return this.#refreshToken; + } + // --- DESTROY --- public destroy(): void { this.#idTokenSubs.unsubscribe(); this.#accessTokenSubs.unsubscribe(); + this.#refreshTokenSubs.unsubscribe(); this.#userProfileSubs.unsubscribe(); this.#userSessionSubs.unsubscribe(); this.#authenticatedSubs.unsubscribe(); @@ -289,6 +310,10 @@ export class OIDCAuthManager extends AuthManager { return this.#accessTokenSubs.add(handler); } + public onRefreshTokenChanged(handler: AuthSubscriber<[string | undefined]>): AuthSubscription { + return this.#refreshTokenSubs.add(handler); + } + public onUserProfileChanged(handler: AuthSubscriber<[UserProfile | undefined]>): AuthSubscription { return this.#userProfileSubs.add(handler); } diff --git a/projects/ngx-auth/core/auth.service.ts b/projects/ngx-auth/core/auth.service.ts index e3fd2870..ebf568bf 100644 --- a/projects/ngx-auth/core/auth.service.ts +++ b/projects/ngx-auth/core/auth.service.ts @@ -20,6 +20,7 @@ export class AuthService implements OnDestroy { #idToken$: ReplaySubject = new ReplaySubject(1); #accessToken$: ReplaySubject = new ReplaySubject(1); + #refreshToken$: ReplaySubject = new ReplaySubject(1); #userProfile$: ReplaySubject = new ReplaySubject(1); #userSession$: ReplaySubject = new ReplaySubject(1); #isAuthenticated$: ReplaySubject = new ReplaySubject(1); @@ -77,6 +78,11 @@ export class AuthService implements OnDestroy { distinctUntilChanged(), map(token => AuthUtils.decodeJwt(token)) ); + + public readonly refreshToken$: Observable = + this.#refreshToken$.asObservable().pipe( + distinctUntilChanged() + ); /* eslint-enable @typescript-eslint/member-ordering */ // --- OIDCAuthManager --- @@ -165,12 +171,20 @@ export class AuthService implements OnDestroy { return this.#manager.getAccessTokenDecoded(); } + /** + * @see {@link OIDCAuthManager.getRefreshToken} + */ + public async getRefreshToken(): Promise { + return this.#manager.getRefreshToken(); + } + // --- HELPER(s) ---- #listenForManagerChanges(): void { this.#authManagerSubs.push( this.#manager.onIdTokenChanged(value => this.#ngZone.run(() => this.#idToken$.next(value))), this.#manager.onAccessTokenChanged(value => this.#ngZone.run(() => this.#accessToken$.next(value))), + this.#manager.onRefreshTokenChanged(value => this.#ngZone.run(() => this.#refreshToken$.next(value))), this.#manager.onUserProfileChanged(value => this.#ngZone.run(() => this.#userProfile$.next(value))), this.#manager.onUserSessionChanged(value => this.#ngZone.run(() => this.#userSession$.next(value))), this.#manager.onAuthenticatedChanged(value => this.#ngZone.run(() => this.#isAuthenticated$.next(value))),