diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.spec.ts index 9973dc3e2..0431f88d7 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.spec.ts @@ -78,5 +78,23 @@ describe('RefreshSessionCallbackHandlerService', () => { }, }); })); + + it('does not overwrite the stored nonce when a refresh token exists', waitForAsync(() => { + spyOn( + flowsDataService, + 'getExistingOrCreateAuthStateControl' + ).and.returnValue('state-data'); + spyOn(authStateService, 'getRefreshToken').and.returnValue( + 'henlo-furiend' + ); + spyOn(authStateService, 'getIdToken').and.returnValue('henlo-legger'); + const setNonceSpy = spyOn(flowsDataService, 'setNonce'); + + service + .refreshSessionWithRefreshTokens({ configId: 'configId1' }) + .subscribe(() => { + expect(setNonceSpy).not.toHaveBeenCalled(); + }); + })); }); }); diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.ts index 40da39f2a..8ad10dacd 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/refresh-session-callback-handler.service.ts @@ -3,7 +3,6 @@ import { Observable, of, throwError } from 'rxjs'; import { AuthStateService } from '../../auth-state/auth-state.service'; import { OpenIdConfiguration } from '../../config/openid-configuration'; import { LoggerService } from '../../logging/logger.service'; -import { TokenValidationService } from '../../validation/token-validation.service'; import { CallbackContext } from '../callback-context'; import { FlowsDataService } from '../flows-data.service'; @@ -44,11 +43,6 @@ export class RefreshSessionCallbackHandlerService { config, 'found refresh code, obtaining new credentials with refresh code' ); - // Nonce is not used with refresh tokens; but Key cloak may send it anyway - this.flowsDataService.setNonce( - TokenValidationService.refreshTokenNoncePlaceholder, - config - ); return of(callbackContext); } else { diff --git a/projects/angular-auth-oidc-client/src/lib/validation/state-validation.service.ts b/projects/angular-auth-oidc-client/src/lib/validation/state-validation.service.ts index 1d70c1602..34007f1fc 100644 --- a/projects/angular-auth-oidc-client/src/lib/validation/state-validation.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/validation/state-validation.service.ts @@ -139,7 +139,8 @@ export class StateValidationService { toReturn.decodedIdToken, authNonce, Boolean(ignoreNonceAfterRefresh), - configuration + configuration, + isInRefreshTokenFlow ) ) { this.loggerService.logWarning( diff --git a/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.spec.ts index 8df62ee34..611d1b9c3 100644 --- a/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.spec.ts @@ -111,56 +111,52 @@ describe('TokenValidationService', () => { ).toBe(false); }); - it('should validate id token nonce after refresh token grant when undefined and no ignore', () => { + it('validates refresh-token flow when id_token contains no nonce (spec-compliant IdP)', () => { expect( tokenValidationService.validateIdTokenNonce( { nonce: undefined }, - TokenValidationService.refreshTokenNoncePlaceholder, + 'realNonce', false, - { - configId: 'configId1', - } + { configId: 'configId1' }, + true ) ).toBe(true); }); - it('should validate id token nonce after refresh token grant when undefined and ignore', () => { + it('validates refresh-token flow when id_token nonce matches the original nonce (Keycloak case)', () => { expect( tokenValidationService.validateIdTokenNonce( - { nonce: undefined }, - TokenValidationService.refreshTokenNoncePlaceholder, - true, - { - configId: 'configId1', - } + { nonce: 'realNonce' }, + 'realNonce', + false, + { configId: 'configId1' }, + true ) ).toBe(true); }); - it('should validate id token nonce after refresh token grant when defined and ignore', () => { + it('rejects refresh-token flow when id_token nonce differs from original and ignoreNonceAfterRefresh is false', () => { expect( tokenValidationService.validateIdTokenNonce( - { nonce: 'test1' }, - TokenValidationService.refreshTokenNoncePlaceholder, - true, - { - configId: 'configId1', - } + { nonce: 'tamperedNonce' }, + 'realNonce', + false, + { configId: 'configId1' }, + true ) - ).toBe(true); + ).toBe(false); }); - it('should not validate id token nonce after refresh token grant when defined and no ignore', () => { + it('validates refresh-token flow when ignoreNonceAfterRefresh is true regardless of returned nonce', () => { expect( tokenValidationService.validateIdTokenNonce( - { nonce: 'test1' }, - TokenValidationService.refreshTokenNoncePlaceholder, - false, - { - configId: 'configId1', - } + { nonce: 'anythingFromIdP' }, + 'realNonce', + true, + { configId: 'configId1' }, + true ) - ).toBe(false); + ).toBe(true); }); }); diff --git a/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.ts b/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.ts index df4779fd2..41d78713e 100644 --- a/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/validation/token-validation.service.ts @@ -56,6 +56,11 @@ import { alg2kty, getImportAlg, getVerifyAlg } from './token-validation.helper'; @Injectable({ providedIn: 'root' }) export class TokenValidationService { + /** + * @deprecated No longer written to storage. Kept only so external + * consumers that previously imported the symbol still compile. + * Will be removed in a future major release. + */ static refreshTokenNoncePlaceholder = '--RefreshToken--'; keyAlgorithms: string[] = [ @@ -282,13 +287,34 @@ export class TokenValidationService { dataIdToken: any, localNonce: any, ignoreNonceAfterRefresh: boolean, - configuration: OpenIdConfiguration + configuration: OpenIdConfiguration, + isRefreshTokenFlow = false ): boolean { - const isFromRefreshToken = - (dataIdToken.nonce === undefined || ignoreNonceAfterRefresh) && - localNonce === TokenValidationService.refreshTokenNoncePlaceholder; + if (isRefreshTokenFlow) { + if (dataIdToken.nonce === undefined) { + return true; + } + + if (ignoreNonceAfterRefresh) { + return true; + } + + if (dataIdToken.nonce === localNonce) { + return true; + } + + this.loggerService.logDebug( + configuration, + 'Validate_id_token_nonce failed in refresh-token flow, dataIdToken.nonce: ' + + dataIdToken.nonce + + ' local_nonce:' + + localNonce + ); + + return false; + } - if (!isFromRefreshToken && dataIdToken.nonce !== localNonce) { + if (dataIdToken.nonce !== localNonce) { this.loggerService.logDebug( configuration, 'Validate_id_token_nonce failed, dataIdToken.nonce: ' +