feat: init and fork some code from angular-auth-oidc-client
Some checks are pending
Build, Lint & Test Lib / Built, Lint and Test Library (push) Waiting to run
Build, Lint & Test Lib / Angular latest (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest Standalone & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular 16 & RxJs 6 (push) Blocked by required conditions
Build, Lint & Test Lib / Angular V16 (push) Blocked by required conditions
Docs / Build and Deploy Docs Job (push) Waiting to run
Docs / Close Pull Request Job (push) Waiting to run

This commit is contained in:
2025-01-18 01:05:00 +08:00
parent 276d9fbda8
commit 983254164b
201 changed files with 35689 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { base64url } from 'rfc4648';
import { CryptoService } from '../utils/crypto/crypto.service';
import { JwkWindowCryptoService } from './jwk-window-crypto.service';
describe('JwkWindowCryptoService', () => {
let service: JwkWindowCryptoService;
const alg = {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256',
};
const key1 = {
kty: 'RSA',
use: 'sig',
kid: '5626CE6A8F4F5FCD79C6642345282CA76D337548RS256',
x5t: 'VibOao9PX815xmQjRSgsp20zdUg',
e: 'AQAB',
n: 'uu3-HK4pLRHJHoEBzFhM516RWx6nybG5yQjH4NbKjfGQ8dtKy1BcGjqfMaEKF8KOK44NbAx7rtBKCO9EKNYkeFvcUzBzVeuu4jWG61XYdTekgv-Dh_Fj8245GocEkbvBbFW6cw-_N59JWqUuiCvb-EOfhcuubUcr44a0AQyNccYNpcXGRcMKy7_L1YhO0AMULqLDDVLFj5glh4TcJ2N5VnJedq1-_JKOxPqD1ni26UOQoWrW16G29KZ1_4Xxf2jX8TAq-4RJEHccdzgZVIO4F5B4MucMZGq8_jMCpiTUsUGDOAMA_AmjxIRHOtO5n6Pt0wofrKoAVhGh2sCTtaQf2Q',
x5c: [
'MIIDPzCCAiegAwIBAgIQF+HRVxLHII9IlOoQk6BxcjANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDDBBzdHMuZGV2LmNlcnQuY29tMB4XDTE5MDIyMDEwMTA0M1oXDTM5MDIyMDEwMTkyOVowGzEZMBcGA1UEAwwQc3RzLmRldi5jZXJ0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrt/hyuKS0RyR6BAcxYTOdekVsep8mxuckIx+DWyo3xkPHbSstQXBo6nzGhChfCjiuODWwMe67QSgjvRCjWJHhb3FMwc1XrruI1hutV2HU3pIL/g4fxY/NuORqHBJG7wWxVunMPvzefSVqlLogr2/hDn4XLrm1HK+OGtAEMjXHGDaXFxkXDCsu/y9WITtADFC6iww1SxY+YJYeE3CdjeVZyXnatfvySjsT6g9Z4tulDkKFq1tehtvSmdf+F8X9o1/EwKvuESRB3HHc4GVSDuBeQeDLnDGRqvP4zAqYk1LFBgzgDAPwJo8SERzrTuZ+j7dMKH6yqAFYRodrAk7WkH9kCAwEAAaN/MH0wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAtBgNVHREEJjAkghBzdHMuZGV2LmNlcnQuY29tghBzdHMuZGV2LmNlcnQuY29tMB0GA1UdDgQWBBQuyHxWP3je6jGMOmOiY+hz47r36jANBgkqhkiG9w0BAQsFAAOCAQEAKEHG7Ga6nb2XiHXDc69KsIJwbO80+LE8HVJojvITILz3juN6/FmK0HmogjU6cYST7m1MyxsVhQQNwJASZ6haBNuBbNzBXfyyfb4kr62t1oDLNwhctHaHaM4sJSf/xIw+YO+Qf7BtfRAVsbM05+QXIi2LycGrzELiXu7KFM0E1+T8UOZ2Qyv7OlCb/pWkYuDgE4w97ox0MhDpvgluxZLpRanOLUCVGrfFaij7gRAhjYPUY3vAEcD8JcFBz1XijU8ozRO6FaG4qg8/JCe+VgoWsMDj3sKB9g0ob6KCyG9L2bdk99PGgvXDQvMYCpkpZzG3XsxOINPd5p0gc209ZOoxTg==',
],
alg: 'RS256',
} as JsonWebKey;
const key2 = {
kty: 'RSA',
n: 'wq0vJv4Xl2xSQTN75_N4JeFHlHH80PytypJqyNrhWIp1P9Ur4-5QSiS8BI8PYSh0dQy4NMoj9YMRcyge3y81uCCwxouePiAGc0xPy6QkAOiinvV3KJEMtbppicOvZEzMXb3EqRM-9Twxbp2hhBAPSAhyL79Rwy4JuIQ6imaqL0NIEGv8_BOe_twMPOLGTJhepDO6kDs6O0qlLgPRHQVuKAz3afVby0C2myDLpo5YaI66arU9VXXGQtIp8MhBY9KbsGaYskejSWhSBOcwdtYMEo5rXWGGVnrHiSqq8mm-sVXLQBe5xPFBs4IQ_Gz4nspr05LEEbsHSwFyGq5D77XPxGUPDCq5ZVvON0yBizaHcJ-KA0Lw6uXtOH9-YyVGuaBynkrQEo3pP2iy1uWt-TiQPb8PMsCAdWZP-6R0QKHtjds9HmjIkgFTJSTIeETjNck_bB4ud79gZT-INikjPFTTeyQYk2jqxEJanVe3k0i_1vpskRpknJ7F2vTL45LAQkjWvczjWmHxGA5D4-1msuylXpY8Y4WxnUq6dRTEN29IRVCil9Mfp6JMsquFGTvJO0-Ffl0_suMZZl3uXNt23E9vGreByalWHivYmfpIor5Q5JaFKekRVV-U1KDBaeQQaHp_VqliUKImdUE9-GXNOIaBMjRvfy0nxsRe_q_dD6jc_GU',
e: 'AQAB',
} as JsonWebKey;
const key3 = {
kty: 'RSA',
n: 'u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw',
e: 'AQAB',
alg: 'RS256',
kid: 'boop',
use: 'sig',
} as JsonWebKey;
const keys: JsonWebKey[] = [key1, key2, key3];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [JwkWindowCryptoService, CryptoService],
});
});
beforeEach(waitForAsync(() => {
service = TestBed.inject(JwkWindowCryptoService);
}));
it('should create', () => {
expect(service).toBeTruthy();
});
describe('importVerificationKey', () => {
it('returns instance of CryptoKey when valid input is provided', (done) => {
const promises = keys.map((key) =>
service.importVerificationKey(key, alg)
);
Promise.all(promises).then((values) => {
values.forEach((value) => {
expect(value).toBeInstanceOf(CryptoKey);
});
done();
});
});
});
describe('verifyKey', () => {
it('returns true when valid input is provided', (done) => {
const headerAndPayloadString =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0';
const signatureString =
'NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ';
const signature: Uint8Array = base64url.parse(signatureString, {
loose: true,
});
service
.importVerificationKey(key3, alg)
.then((c) =>
service.verifyKey(alg, c, signature, headerAndPayloadString)
)
.then((value) => {
expect(value).toEqual(true);
})
.finally(() => {
done();
});
});
});
});

View File

@@ -0,0 +1,38 @@
import { inject, Injectable } from 'injection-js';
import { CryptoService } from '../utils/crypto/crypto.service';
@Injectable()
export class JwkWindowCryptoService {
private readonly cryptoService = inject(CryptoService);
importVerificationKey(
key: JsonWebKey,
algorithm:
| AlgorithmIdentifier
| RsaHashedImportParams
| EcKeyImportParams
| HmacImportParams
| AesKeyAlgorithm
| null
): Promise<CryptoKey> {
return this.cryptoService
.getCrypto()
.subtle.importKey('jwk', key, algorithm, false, ['verify']);
}
verifyKey(
verifyAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams | null,
cryptoKey: CryptoKey,
signature: BufferSource,
signingInput: string
): Promise<boolean> {
return this.cryptoService
.getCrypto()
.subtle.verify(
verifyAlgorithm,
cryptoKey,
signature,
new TextEncoder().encode(signingInput)
);
}
}

View File

@@ -0,0 +1,35 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { CryptoService } from '../utils/crypto/crypto.service';
import { JwtWindowCryptoService } from './jwt-window-crypto.service';
describe('JwtWindowCryptoService', () => {
let service: JwtWindowCryptoService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [JwtWindowCryptoService, CryptoService],
});
});
beforeEach(() => {
service = TestBed.inject(JwtWindowCryptoService);
});
it('should create', () => {
expect(service).toBeTruthy();
});
describe('generateCodeChallenge', () => {
it('returns good result with correct codeVerifier', waitForAsync(() => {
const outcome = 'R2TWD45Vtcf_kfAqjuE3LMSRF3JDE5fsFndnn6-a0nQ';
const observable = service.generateCodeChallenge(
'44445543344242132145455aaabbdc3b4'
);
observable.subscribe((value) => {
expect(value).toBe(outcome);
});
}));
});
});

View File

@@ -0,0 +1,63 @@
import { inject, Injectable } from 'injection-js';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { CryptoService } from '../utils/crypto/crypto.service';
@Injectable()
export class JwtWindowCryptoService {
private readonly cryptoService = inject(CryptoService);
generateCodeChallenge(codeVerifier: string): Observable<string> {
return this.calcHash(codeVerifier).pipe(
map((challengeRaw: string) => this.base64UrlEncode(challengeRaw))
);
}
generateAtHash(accessToken: string, algorithm: string): Observable<string> {
return this.calcHash(accessToken, algorithm).pipe(
map((tokenHash) => {
const substr: string = tokenHash.substr(0, tokenHash.length / 2);
const tokenHashBase64: string = btoa(substr);
return tokenHashBase64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
})
);
}
private calcHash(
valueToHash: string,
algorithm = 'SHA-256'
): Observable<string> {
const msgBuffer: Uint8Array = new TextEncoder().encode(valueToHash);
return from(
this.cryptoService.getCrypto().subtle.digest(algorithm, msgBuffer)
).pipe(
map((hashBuffer: unknown) => {
const buffer = hashBuffer as ArrayBuffer;
const hashArray: number[] = Array.from(new Uint8Array(buffer));
return this.toHashString(hashArray);
})
);
}
private toHashString(byteArray: number[]): string {
let result = '';
for (const e of byteArray) {
result += String.fromCharCode(e);
}
return result;
}
private base64UrlEncode(str: string): string {
const base64: string = btoa(str);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
}

13
src/validation/jwtkeys.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface JwtKeys {
keys: JwtKey[];
}
export interface JwtKey {
kty: string;
use: string;
kid: string;
x5t: string;
e: string;
n: string;
x5c: any[];
}

View File

@@ -0,0 +1,13 @@
import { ValidationResult } from './validation-result';
export class StateValidationResult {
constructor(
public accessToken = '',
public idToken = '',
public authResponseIsValid = false,
public decodedIdToken: any = {
at_hash: '',
},
public state: ValidationResult = ValidationResult.NotSet
) {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,539 @@
import { inject, Injectable } from 'injection-js';
import { Observable, of } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context';
import { LoggerService } from '../logging/logger.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { EqualityService } from '../utils/equality/equality.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { TokenHelperService } from '../utils/tokenHelper/token-helper.service';
import { StateValidationResult } from './state-validation-result';
import { TokenValidationService } from './token-validation.service';
import { ValidationResult } from './validation-result';
@Injectable()
export class StateValidationService {
private readonly storagePersistenceService = inject(
StoragePersistenceService
);
private readonly tokenValidationService = inject(TokenValidationService);
private readonly tokenHelperService = inject(TokenHelperService);
private readonly loggerService = inject(LoggerService);
private readonly equalityService = inject(EqualityService);
private readonly flowHelper = inject(FlowHelper);
getValidatedStateResult(
callbackContext: CallbackContext,
configuration: OpenIdConfiguration
): Observable<StateValidationResult> {
const hasError = Boolean(callbackContext.authResult?.error);
const hasCallbackContext = Boolean(callbackContext);
if (!hasCallbackContext || hasError) {
return of(new StateValidationResult('', '', false, {}));
}
return this.validateState(callbackContext, configuration);
}
private validateState(
callbackContext: CallbackContext,
configuration: OpenIdConfiguration
): Observable<StateValidationResult> {
const toReturn = new StateValidationResult();
const authStateControl = this.storagePersistenceService.read(
'authStateControl',
configuration
);
if (
!this.tokenValidationService.validateStateFromHashCallback(
callbackContext.authResult?.state,
authStateControl,
configuration
)
) {
this.loggerService.logWarning(
configuration,
'authCallback incorrect state'
);
toReturn.state = ValidationResult.StatesDoNotMatch;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
const isCurrentFlowImplicitFlowWithAccessToken =
this.flowHelper.isCurrentFlowImplicitFlowWithAccessToken(configuration);
const isCurrentFlowCodeFlow =
this.flowHelper.isCurrentFlowCodeFlow(configuration);
if (isCurrentFlowImplicitFlowWithAccessToken || isCurrentFlowCodeFlow) {
toReturn.accessToken = callbackContext.authResult?.access_token ?? '';
}
const disableIdTokenValidation = configuration.disableIdTokenValidation;
if (disableIdTokenValidation) {
toReturn.state = ValidationResult.Ok;
toReturn.authResponseIsValid = true;
return of(toReturn);
}
const isInRefreshTokenFlow =
callbackContext.isRenewProcess && !!callbackContext.refreshToken;
const hasIdToken = Boolean(callbackContext.authResult?.id_token);
if (isInRefreshTokenFlow && !hasIdToken) {
toReturn.state = ValidationResult.Ok;
toReturn.authResponseIsValid = true;
return of(toReturn);
}
if (hasIdToken) {
const {
clientId,
issValidationOff,
maxIdTokenIatOffsetAllowedInSeconds,
disableIatOffsetValidation,
ignoreNonceAfterRefresh,
renewTimeBeforeTokenExpiresInSeconds,
} = configuration;
toReturn.idToken = callbackContext.authResult?.id_token ?? '';
toReturn.decodedIdToken = this.tokenHelperService.getPayloadFromToken(
toReturn.idToken,
false,
configuration
);
return this.tokenValidationService
.validateSignatureIdToken(
toReturn.idToken,
callbackContext.jwtKeys,
configuration
)
.pipe(
mergeMap((isSignatureIdTokenValid: boolean) => {
if (!isSignatureIdTokenValid) {
this.loggerService.logDebug(
configuration,
'authCallback Signature validation failed id_token'
);
toReturn.state = ValidationResult.SignatureFailed;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
const authNonce = this.storagePersistenceService.read(
'authNonce',
configuration
);
if (
!this.tokenValidationService.validateIdTokenNonce(
toReturn.decodedIdToken,
authNonce,
Boolean(ignoreNonceAfterRefresh),
configuration
)
) {
this.loggerService.logWarning(
configuration,
'authCallback incorrect nonce, did you call the checkAuth() method multiple times?'
);
toReturn.state = ValidationResult.IncorrectNonce;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!this.tokenValidationService.validateRequiredIdToken(
toReturn.decodedIdToken,
configuration
)
) {
this.loggerService.logDebug(
configuration,
'authCallback Validation, one of the REQUIRED properties missing from id_token'
);
toReturn.state = ValidationResult.RequiredPropertyMissing;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!isInRefreshTokenFlow &&
!this.tokenValidationService.validateIdTokenIatMaxOffset(
toReturn.decodedIdToken,
maxIdTokenIatOffsetAllowedInSeconds ?? 120,
Boolean(disableIatOffsetValidation),
configuration
)
) {
this.loggerService.logWarning(
configuration,
'authCallback Validation, iat rejected id_token was issued too far away from the current time'
);
toReturn.state = ValidationResult.MaxOffsetExpired;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
const authWellKnownEndPoints = this.storagePersistenceService.read(
'authWellKnownEndPoints',
configuration
);
if (authWellKnownEndPoints) {
if (issValidationOff) {
this.loggerService.logDebug(
configuration,
'iss validation is turned off, this is not recommended!'
);
} else if (
!issValidationOff &&
!this.tokenValidationService.validateIdTokenIss(
toReturn.decodedIdToken,
authWellKnownEndPoints.issuer,
configuration
)
) {
this.loggerService.logWarning(
configuration,
'authCallback incorrect iss does not match authWellKnownEndpoints issuer'
);
toReturn.state = ValidationResult.IssDoesNotMatchIssuer;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
} else {
this.loggerService.logWarning(
configuration,
'authWellKnownEndpoints is undefined'
);
toReturn.state = ValidationResult.NoAuthWellKnownEndPoints;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!this.tokenValidationService.validateIdTokenAud(
toReturn.decodedIdToken,
clientId,
configuration
)
) {
this.loggerService.logWarning(
configuration,
'authCallback incorrect aud'
);
toReturn.state = ValidationResult.IncorrectAud;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!this.tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(
toReturn.decodedIdToken
)
) {
this.loggerService.logWarning(
configuration,
'authCallback missing azp'
);
toReturn.state = ValidationResult.IncorrectAzp;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!this.tokenValidationService.validateIdTokenAzpValid(
toReturn.decodedIdToken,
clientId
)
) {
this.loggerService.logWarning(
configuration,
'authCallback incorrect azp'
);
toReturn.state = ValidationResult.IncorrectAzp;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!this.isIdTokenAfterRefreshTokenRequestValid(
callbackContext,
toReturn.decodedIdToken,
configuration
)
) {
this.loggerService.logWarning(
configuration,
'authCallback pre, post id_token claims do not match in refresh'
);
toReturn.state =
ValidationResult.IncorrectIdTokenClaimsAfterRefresh;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
if (
!isInRefreshTokenFlow &&
!this.tokenValidationService.validateIdTokenExpNotExpired(
toReturn.decodedIdToken,
configuration,
renewTimeBeforeTokenExpiresInSeconds
)
) {
this.loggerService.logWarning(
configuration,
'authCallback id token expired'
);
toReturn.state = ValidationResult.TokenExpired;
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
return this.validateDefault(
isCurrentFlowImplicitFlowWithAccessToken,
isCurrentFlowCodeFlow,
toReturn,
configuration,
callbackContext
);
})
);
} else {
this.loggerService.logDebug(
configuration,
'No id_token found, skipping id_token validation'
);
}
return this.validateDefault(
isCurrentFlowImplicitFlowWithAccessToken,
isCurrentFlowCodeFlow,
toReturn,
configuration,
callbackContext
);
}
private validateDefault(
isCurrentFlowImplicitFlowWithAccessToken: boolean,
isCurrentFlowCodeFlow: boolean,
toReturn: StateValidationResult,
configuration: OpenIdConfiguration,
callbackContext: CallbackContext
): Observable<StateValidationResult> {
// flow id_token
if (!isCurrentFlowImplicitFlowWithAccessToken && !isCurrentFlowCodeFlow) {
toReturn.authResponseIsValid = true;
toReturn.state = ValidationResult.Ok;
this.handleSuccessfulValidation(configuration);
this.handleUnsuccessfulValidation(configuration);
return of(toReturn);
}
// only do check if id_token returned, no always the case when using refresh tokens
if (callbackContext.authResult?.id_token) {
const idTokenHeader = this.tokenHelperService.getHeaderFromToken(
toReturn.idToken,
false,
configuration
);
if (
isCurrentFlowCodeFlow &&
!(toReturn.decodedIdToken.at_hash as string)
) {
this.loggerService.logDebug(
configuration,
'Code Flow active, and no at_hash in the id_token, skipping check!'
);
} else {
return this.tokenValidationService
.validateIdTokenAtHash(
toReturn.accessToken,
toReturn.decodedIdToken.at_hash,
idTokenHeader.alg, // 'RS256'
configuration
)
.pipe(
map((valid: boolean) => {
if (!valid || !toReturn.accessToken) {
this.loggerService.logWarning(
configuration,
'authCallback incorrect at_hash'
);
toReturn.state = ValidationResult.IncorrectAtHash;
this.handleUnsuccessfulValidation(configuration);
return toReturn;
} else {
toReturn.authResponseIsValid = true;
toReturn.state = ValidationResult.Ok;
this.handleSuccessfulValidation(configuration);
return toReturn;
}
})
);
}
}
toReturn.authResponseIsValid = true;
toReturn.state = ValidationResult.Ok;
this.handleSuccessfulValidation(configuration);
return of(toReturn);
}
private isIdTokenAfterRefreshTokenRequestValid(
callbackContext: CallbackContext,
newIdToken: any,
configuration: OpenIdConfiguration
): boolean {
const { useRefreshToken, disableRefreshIdTokenAuthTimeValidation } =
configuration;
if (!useRefreshToken) {
return true;
}
if (!callbackContext.existingIdToken) {
return true;
}
const decodedIdToken = this.tokenHelperService.getPayloadFromToken(
callbackContext.existingIdToken,
false,
configuration
);
// Upon successful validation of the Refresh Token, the response body is the Token Response of Section 3.1.3.3
// except that it might not contain an id_token.
// If an ID Token is returned as a result of a token refresh request, the following requirements apply:
// its iss Claim Value MUST be the same as in the ID Token issued when the original authentication occurred,
if (decodedIdToken.iss !== newIdToken.iss) {
this.loggerService.logDebug(
configuration,
`iss do not match: ${decodedIdToken.iss} ${newIdToken.iss}`
);
return false;
}
// its azp Claim Value MUST be the same as in the ID Token issued when the original authentication occurred;
// if no azp Claim was present in the original ID Token, one MUST NOT be present in the new ID Token, and
// otherwise, the same rules apply as apply when issuing an ID Token at the time of the original authentication.
if (decodedIdToken.azp !== newIdToken.azp) {
this.loggerService.logDebug(
configuration,
`azp do not match: ${decodedIdToken.azp} ${newIdToken.azp}`
);
return false;
}
// its sub Claim Value MUST be the same as in the ID Token issued when the original authentication occurred,
if (decodedIdToken.sub !== newIdToken.sub) {
this.loggerService.logDebug(
configuration,
`sub do not match: ${decodedIdToken.sub} ${newIdToken.sub}`
);
return false;
}
// its aud Claim Value MUST be the same as in the ID Token issued when the original authentication occurred,
if (
!this.equalityService.isStringEqualOrNonOrderedArrayEqual(
decodedIdToken?.aud,
newIdToken?.aud
)
) {
this.loggerService.logDebug(
configuration,
`aud in new id_token is not valid: '${decodedIdToken?.aud}' '${newIdToken.aud}'`
);
return false;
}
if (disableRefreshIdTokenAuthTimeValidation) {
return true;
}
// its iat Claim MUST represent the time that the new ID Token is issued,
// if the ID Token contains an auth_time Claim, its value MUST represent the time of the original authentication
// - not the time that the new ID token is issued,
if (decodedIdToken.auth_time !== newIdToken.auth_time) {
this.loggerService.logDebug(
configuration,
`auth_time do not match: ${decodedIdToken.auth_time} ${newIdToken.auth_time}`
);
return false;
}
return true;
}
private handleSuccessfulValidation(configuration: OpenIdConfiguration): void {
const { autoCleanStateAfterAuthentication } = configuration;
this.storagePersistenceService.write('authNonce', null, configuration);
if (autoCleanStateAfterAuthentication) {
this.storagePersistenceService.write(
'authStateControl',
'',
configuration
);
}
this.loggerService.logDebug(
configuration,
'authCallback token(s) validated, continue'
);
}
private handleUnsuccessfulValidation(
configuration: OpenIdConfiguration
): void {
const { autoCleanStateAfterAuthentication } = configuration;
this.storagePersistenceService.write('authNonce', null, configuration);
if (autoCleanStateAfterAuthentication) {
this.storagePersistenceService.write(
'authStateControl',
'',
configuration
);
}
this.loggerService.logDebug(configuration, 'authCallback token(s) invalid');
}
}

View File

@@ -0,0 +1,159 @@
import { alg2kty, getImportAlg, getVerifyAlg } from './token-validation.helper';
describe('getVerifyAlg', () => {
it('returns null if char has no E or R', () => {
const algorithm = 'ASDFGT';
const result = getVerifyAlg(algorithm);
expect(result).toBe(null);
});
it('returns correct result when algorithm is R', () => {
const algorithm = 'R';
const result = getVerifyAlg(algorithm);
expect(result).toEqual({
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256',
});
});
it('returns null if algorithm is only E', () => {
const algorithm = 'E';
const result = getVerifyAlg(algorithm);
expect(result).toBe(null);
});
it('returns correct result if algorithm is E256', () => {
const algorithm = 'E256';
const result = getVerifyAlg(algorithm);
expect(result).toEqual({
name: 'ECDSA',
hash: 'SHA-256',
});
});
it('returns correct result if algorithm is E384', () => {
const algorithm = 'E384';
const result = getVerifyAlg(algorithm);
expect(result).toEqual({
name: 'ECDSA',
hash: 'SHA-384',
});
});
});
describe('alg2kty', () => {
it('returns correct result if algorithm is R', () => {
const algorithm = 'R';
const result = alg2kty(algorithm);
expect(result).toEqual('RSA');
});
it('returns correct result if algorithm is E', () => {
const algorithm = 'E';
const result = alg2kty(algorithm);
expect(result).toEqual('EC');
});
it('returns correct result if algorithm is E', () => {
const algorithm = 'SOMETHING_ELSE';
expect(() => alg2kty(algorithm)).toThrow(
new Error('Cannot infer kty from alg: SOMETHING_ELSE')
);
});
});
describe('getImportAlg', () => {
it('returns null if algorithm is not R or E', () => {
const algorithm = 'Q';
const result = getImportAlg(algorithm);
expect(result).toBe(null);
});
it('returns null if algorithm is only R', () => {
const algorithm = 'R';
const result = getImportAlg(algorithm);
expect(result).toBe(null);
});
it('returns correct result if algorithm is R256', () => {
const algorithm = 'R256';
const result = getImportAlg(algorithm);
expect(result).toEqual({
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256',
});
});
it('returns correct result if algorithm is R384', () => {
const algorithm = 'R384';
const result = getImportAlg(algorithm);
expect(result).toEqual({
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-384',
});
});
it('returns correct result if algorithm is R512', () => {
const algorithm = 'R512';
const result = getImportAlg(algorithm);
expect(result).toEqual({
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-512',
});
});
it('returns null if algorithm is only E', () => {
const algorithm = 'E';
const result = getImportAlg(algorithm);
expect(result).toBe(null);
});
it('returns correct result if algorithm is E256', () => {
const algorithm = 'E256';
const result = getImportAlg(algorithm);
expect(result).toEqual({
name: 'ECDSA',
namedCurve: 'P-256',
});
});
it('returns correct result if algorithm is E384', () => {
const algorithm = 'E384';
const result = getImportAlg(algorithm);
expect(result).toEqual({
name: 'ECDSA',
namedCurve: 'P-384',
});
});
});

View File

@@ -0,0 +1,82 @@
export function getVerifyAlg(
alg: string
): RsaHashedImportParams | EcdsaParams | null {
switch (alg.charAt(0)) {
case 'R':
return {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256',
};
case 'E':
if (alg.includes('256')) {
return {
name: 'ECDSA',
hash: 'SHA-256',
};
} else if (alg.includes('384')) {
return {
name: 'ECDSA',
hash: 'SHA-384',
};
} else {
return null;
}
default:
return null;
}
}
export function alg2kty(alg: string): string {
switch (alg.charAt(0)) {
case 'R':
return 'RSA';
case 'E':
return 'EC';
default:
throw new Error('Cannot infer kty from alg: ' + alg);
}
}
export function getImportAlg(
alg: string
): RsaHashedImportParams | EcKeyImportParams | null {
switch (alg.charAt(0)) {
case 'R':
if (alg.includes('256')) {
return {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256',
};
} else if (alg.includes('384')) {
return {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-384',
};
} else if (alg.includes('512')) {
return {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-512',
};
} else {
return null;
}
case 'E':
if (alg.includes('256')) {
return {
name: 'ECDSA',
namedCurve: 'P-256',
};
} else if (alg.includes('384')) {
return {
name: 'ECDSA',
namedCurve: 'P-384',
};
} else {
return null;
}
default:
return null;
}
}

View File

@@ -0,0 +1,862 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { of } from 'rxjs';
import { mockProvider } from '../../test/auto-mock';
import { JwkExtractor } from '../extractors/jwk.extractor';
import { LoggerService } from '../logging/logger.service';
import { CryptoService } from '../utils/crypto/crypto.service';
import { TokenHelperService } from '../utils/tokenHelper/token-helper.service';
import { JwkWindowCryptoService } from './jwk-window-crypto.service';
import { JwtWindowCryptoService } from './jwt-window-crypto.service';
import { TokenValidationService } from './token-validation.service';
describe('TokenValidationService', () => {
let tokenValidationService: TokenValidationService;
let tokenHelperService: TokenHelperService;
let jwtWindowCryptoService: JwtWindowCryptoService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
TokenValidationService,
mockProvider(LoggerService),
mockProvider(TokenHelperService),
JwkExtractor,
JwkWindowCryptoService,
JwtWindowCryptoService,
CryptoService,
],
});
});
beforeEach(() => {
tokenValidationService = TestBed.inject(TokenValidationService);
tokenHelperService = TestBed.inject(TokenHelperService);
jwtWindowCryptoService = TestBed.inject(JwtWindowCryptoService);
});
it('should create', () => {
expect(tokenValidationService).toBeTruthy();
});
describe('validateIdTokenAud', () => {
it('returns true if aud is string and passed aud matches idToken.aud', () => {
const dataIdToken = { aud: 'banana' };
const valueTrue = tokenValidationService.validateIdTokenAud(
dataIdToken,
'banana',
{ configId: 'configId1' }
);
expect(valueTrue).toEqual(true);
});
it('returns false if aud is string and passed aud does not match idToken.aud', () => {
const dataIdToken = { aud: 'banana' };
const valueFalse = tokenValidationService.validateIdTokenAud(
dataIdToken,
'bananammmm',
{ configId: 'configId1' }
);
expect(valueFalse).toEqual(false);
});
it('returns true if aud is string array and passed aud is included in the array', () => {
const dataIdToken = {
aud: ['banana', 'apple', 'https://nice.dom'],
};
const audValidTrue = tokenValidationService.validateIdTokenAud(
dataIdToken,
'apple',
{ configId: 'configId1' }
);
expect(audValidTrue).toEqual(true);
});
it('returns false if aud is string array and passed aud is NOT included in the array', () => {
const dataIdToken = {
aud: ['banana', 'apple', 'https://nice.dom'],
};
const audValidFalse = tokenValidationService.validateIdTokenAud(
dataIdToken,
'https://nice.domunnnnnnkoem',
{
configId: 'configId1',
}
);
expect(audValidFalse).toEqual(false);
});
});
describe('validateIdTokenNonce', () => {
it('should validate id token nonce after code grant when match', () => {
expect(
tokenValidationService.validateIdTokenNonce(
{ nonce: 'test1' },
'test1',
false,
{ configId: 'configId1' }
)
).toBe(true);
});
it('should not validate id token nonce after code grant when no match', () => {
expect(
tokenValidationService.validateIdTokenNonce(
{ nonce: 'test1' },
'test2',
false,
{ configId: 'configId1' }
)
).toBe(false);
});
it('should validate id token nonce after refresh token grant when undefined and no ignore', () => {
expect(
tokenValidationService.validateIdTokenNonce(
{ nonce: undefined },
TokenValidationService.refreshTokenNoncePlaceholder,
false,
{
configId: 'configId1',
}
)
).toBe(true);
});
it('should validate id token nonce after refresh token grant when undefined and ignore', () => {
expect(
tokenValidationService.validateIdTokenNonce(
{ nonce: undefined },
TokenValidationService.refreshTokenNoncePlaceholder,
true,
{
configId: 'configId1',
}
)
).toBe(true);
});
it('should validate id token nonce after refresh token grant when defined and ignore', () => {
expect(
tokenValidationService.validateIdTokenNonce(
{ nonce: 'test1' },
TokenValidationService.refreshTokenNoncePlaceholder,
true,
{
configId: 'configId1',
}
)
).toBe(true);
});
it('should not validate id token nonce after refresh token grant when defined and no ignore', () => {
expect(
tokenValidationService.validateIdTokenNonce(
{ nonce: 'test1' },
TokenValidationService.refreshTokenNoncePlaceholder,
false,
{
configId: 'configId1',
}
)
).toBe(false);
});
});
describe('validateIdTokenAzpExistsIfMoreThanOneAud', () => {
it('returns false if aud is array, öength is bigger than 1 and has no azp property', () => {
const dataIdToken = {
aud: ['one', 'two'],
};
const result =
tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(
dataIdToken
);
expect(result).toBe(false);
});
it('returns false if aud is array, ength is bigger than 1 and has no azp property', () => {
const result =
tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(null);
expect(result).toBe(false);
});
});
describe('validateIdTokenAzpValid', () => {
it('returns true dataIdToken param is null', () => {
const result = tokenValidationService.validateIdTokenAzpValid(null, '');
expect(result).toBe(true);
});
it('returns false when aud is an array and client id is NOT in the aud array', () => {
const dataIdToken = {
aud: [
'banana',
'apple',
'188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
],
azp: '188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
};
const azpInvalid = tokenValidationService.validateIdTokenAzpValid(
dataIdToken,
'bananammmm'
);
expect(azpInvalid).toEqual(false);
});
it('returns true when aud is an array and client id is in the aud array', () => {
const dataIdToken = {
aud: [
'banana',
'apple',
'188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
],
azp: '188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
};
const azpValid = tokenValidationService.validateIdTokenAzpValid(
dataIdToken,
'188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com'
);
expect(azpValid).toEqual(true);
});
it('returns true if ID token has no azp property', () => {
const dataIdToken = {
noAzpProperty: 'something',
};
const azpValid = tokenValidationService.validateIdTokenAzpValid(
dataIdToken,
'bananammmm'
);
expect(azpValid).toEqual(true);
});
});
describe('validateIdTokenAzpExistsIfMoreThanOneAud', () => {
it('returns true if aud is array and aud contains azp', () => {
const dataIdToken = {
aud: [
'banana',
'apple',
'188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
],
azp: '188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
};
const valueTrue =
tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(
dataIdToken
);
expect(valueTrue).toEqual(true);
});
it('returns true if aud is array but only has one item', () => {
const dataIdToken = {
aud: ['banana'],
};
const valueTrue =
tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(
dataIdToken
);
expect(valueTrue).toEqual(true);
});
it('returns true if aud is NOT an array', () => {
const dataIdToken = {
aud: 'banana',
};
const valueTrue =
tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(
dataIdToken
);
expect(valueTrue).toEqual(true);
});
});
describe('validateRequiredIdToken', () => {
it('returns false if property iat is missing', () => {
const decodedIdToken = {
iss: 'https://damienbod.b2clogin.com/a0958f45-195b-4036-9259-de2f7e594db6/v2.0/',
sub: 'f836f380-3c64-4802-8dbc-011981c068f5',
aud: 'bad',
exp: 1589210086,
// iat: 1589206486,
};
const result = tokenValidationService.validateRequiredIdToken(
decodedIdToken,
{ configId: 'configId1' }
);
expect(result).toEqual(false);
});
it('returns false if property exp is missing', () => {
const decodedIdToken = {
iss: 'https://damienbod.b2clogin.com/a0958f45-195b-4036-9259-de2f7e594db6/v2.0/',
sub: 'f836f380-3c64-4802-8dbc-011981c068f5',
aud: 'bad',
// exp: 1589210086,
iat: 1589206486,
};
const result = tokenValidationService.validateRequiredIdToken(
decodedIdToken,
{ configId: 'configId1' }
);
expect(result).toEqual(false);
});
it('returns false if property aud is missing', () => {
const decodedIdToken = {
iss: 'https://damienbod.b2clogin.com/a0958f45-195b-4036-9259-de2f7e594db6/v2.0/',
sub: 'f836f380-3c64-4802-8dbc-011981c068f5',
// aud: 'bad',
exp: 1589210086,
iat: 1589206486,
};
const result = tokenValidationService.validateRequiredIdToken(
decodedIdToken,
{ configId: 'configId1' }
);
expect(result).toEqual(false);
});
it('returns false if property sub is missing', () => {
const decodedIdToken = {
iss: 'https://damienbod.b2clogin.com/a0958f45-195b-4036-9259-de2f7e594db6/v2.0/',
// sub: 'f836f380-3c64-4802-8dbc-011981c068f5',
aud: 'bad',
exp: 1589210086,
iat: 1589206486,
};
const result = tokenValidationService.validateRequiredIdToken(
decodedIdToken,
{ configId: 'configId1' }
);
expect(result).toEqual(false);
});
it('returns false if property iss is missing', () => {
const decodedIdToken = {
// iss: 'https://damienbod.b2clogin.com/a0958f45-195b-4036-9259-de2f7e594db6/v2.0/',
sub: 'f836f380-3c64-4802-8dbc-011981c068f5',
aud: 'bad',
exp: 1589210086,
iat: 1589206486,
};
const result = tokenValidationService.validateRequiredIdToken(
decodedIdToken,
{ configId: 'configId1' }
);
expect(result).toEqual(false);
});
it('returns true if all is valid', () => {
const decodedIdToken = {
iss: 'https://damienbod.b2clogin.com/a0958f45-195b-4036-9259-de2f7e594db6/v2.0/',
sub: 'f836f380-3c64-4802-8dbc-011981c068f5',
aud: 'bad',
exp: 1589210086,
iat: 1589206486,
};
const result = tokenValidationService.validateRequiredIdToken(
decodedIdToken,
{ configId: 'configId1' }
);
expect(result).toEqual(true);
});
});
describe('validateIdTokenIss', () => {
it('returns true if issuer matches iss in idToken', () => {
const decodedIdToken = {
iss: 'xc',
};
const valueTrue = tokenValidationService.validateIdTokenIss(
decodedIdToken,
'xc',
{ configId: 'configId1' }
);
expect(valueTrue).toEqual(true);
});
it('returns false if issuer does not match iss in idToken', () => {
const decodedIdToken = {
iss: 'xc',
};
const valueFalse = tokenValidationService.validateIdTokenIss(
decodedIdToken,
'xcjjjj',
{ configId: 'configId1' }
);
expect(valueFalse).toEqual(false);
});
});
describe('validateIdTokenIatMaxOffset', () => {
it('returns true if validationIsDisabled', () => {
const result = tokenValidationService.validateIdTokenIatMaxOffset(
null,
0,
true,
{ configId: 'configId1' }
);
expect(result).toBe(true);
});
it('returns false if dataIdToken has no property "iat"', () => {
const dataIdToken = {
notIat: 'test',
};
const result = tokenValidationService.validateIdTokenIatMaxOffset(
dataIdToken,
0,
false,
{ configId: 'configId1' }
);
expect(result).toBe(false);
});
it('returns true if time is Mon Jan 19 1970 10:26:46 GMT+0100, and the offset is big like 500000000000 seconds', () => {
const decodedIdToken = {
iat: 1589206486, // Mon Jan 19 1970 10:26:46 GMT+0100 (Central European Standard Time)
};
const valueTrue = tokenValidationService.validateIdTokenIatMaxOffset(
decodedIdToken,
500000000000,
false,
{ configId: 'configId1' }
);
expect(valueTrue).toEqual(true);
});
it('returns false if time is Sat Nov 09 1985 02:47:57 GMT+0100, and the offset is 0 seconds', () => {
const decodedIdTokenNegIat = {
iat: 500348877430, // Sat Nov 09 1985 02:47:57 GMT+0100 (Central European Standard Time)
};
const valueFalseNeg = tokenValidationService.validateIdTokenIatMaxOffset(
decodedIdTokenNegIat,
0,
false,
{ configId: 'configId1' }
);
expect(valueFalseNeg).toEqual(false);
});
it('returns true if time is Mon Jan 19 1970 10:26:46 GMT+0100, and the offset is small like 5 seconds', () => {
const decodedIdToken = {
iat: 1589206486, // Mon Jan 19 1970 10:26:46 GMT+0100 (Central European Standard Time)
};
const valueFalse = tokenValidationService.validateIdTokenIatMaxOffset(
decodedIdToken,
5,
false,
{ configId: 'configId1' }
);
expect(valueFalse).toEqual(false);
});
});
describe('validateSignatureIdToken', () => {
it('returns false if no kwtKeys are passed', waitForAsync(() => {
const valueFalse$ = tokenValidationService.validateSignatureIdToken(
'some-id-token',
null,
{ configId: 'configId1' }
);
valueFalse$.subscribe((valueFalse) => {
expect(valueFalse).toEqual(false);
});
}));
it('returns true if no idToken is passed', waitForAsync(() => {
const valueFalse$ = tokenValidationService.validateSignatureIdToken(
null as any,
'some-jwt-keys',
{ configId: 'configId1' }
);
valueFalse$.subscribe((valueFalse) => {
expect(valueFalse).toEqual(true);
});
}));
it('returns false if jwtkeys has no keys-property', waitForAsync(() => {
const valueFalse$ = tokenValidationService.validateSignatureIdToken(
'some-id-token',
{ notKeys: '' },
{ configId: 'configId1' }
);
valueFalse$.subscribe((valueFalse) => {
expect(valueFalse).toEqual(false);
});
}));
it('returns false if header data has no header data', waitForAsync(() => {
spyOn(tokenHelperService, 'getHeaderFromToken').and.returnValue({});
const jwtKeys = {
keys: 'someThing',
};
const valueFalse$ = tokenValidationService.validateSignatureIdToken(
'some-id-token',
jwtKeys,
{ configId: 'configId1' }
);
valueFalse$.subscribe((valueFalse) => {
expect(valueFalse).toEqual(false);
});
}));
it('returns false if header data alg property does not exist in keyalgorithms', waitForAsync(() => {
spyOn(tokenHelperService, 'getHeaderFromToken').and.returnValue({
alg: 'NOT SUPPORTED ALG',
});
const jwtKeys = {
keys: 'someThing',
};
const valueFalse$ = tokenValidationService.validateSignatureIdToken(
'some-id-token',
jwtKeys,
{ configId: 'configId1' }
);
valueFalse$.subscribe((valueFalse) => {
expect(valueFalse).toEqual(false);
});
}));
it('returns false if header data has kid property and jwtKeys has same kid property but they are not valid with the token', (done) => {
const kid = '5626CE6A8F4F5FCD79C6642345282CA76D337548';
spyOn(tokenHelperService, 'getHeaderFromToken').and.returnValue({
alg: 'RS256',
kid,
});
spyOn(tokenHelperService, 'getSignatureFromToken').and.returnValue('');
const jwtKeys = {
keys: [
{
kty: 'RSA',
use: 'sig',
kid,
x5t: 'VibOao9PX815xmQjRSgsp20zdUg',
e: 'AQAB',
n: 'uu3-HK4pLRHJHoEBzFhM516RWx6nybG5yQjH4NbKjfGQ8dtKy1BcGjqfMaEKF8KOK44NbAx7rtBKCO9EKNYkeFvcUzBzVeuu4jWG61XYdTekgv-Dh_Fj8245GocEkbvBbFW6cw-_N59JWqUuiCvb-EOfhcuubUcr44a0AQyNccYNpcXGRcMKy7_L1YhO0AMULqLDDVLFj5glh4TcJ2N5VnJedq1-_JKOxPqD1ni26UOQoWrW16G29KZ1_4Xxf2jX8TAq-4RJEHccdzgZVIO4F5B4MucMZGq8_jMCpiTUsUGDOAMA_AmjxIRHOtO5n6Pt0wofrKoAVhGh2sCTtaQf2Q',
x5c: [
'MIIDPzCCAiegAwIBAgIQF+HRVxLHII9IlOoQk6BxcjANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDDBBzdHMuZGV2LmNlcnQuY29tMB4XDTE5MDIyMDEwMTA0M1oXDTM5MDIyMDEwMTkyOVowGzEZMBcGA1UEAwwQc3RzLmRldi5jZXJ0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrt/hyuKS0RyR6BAcxYTOdekVsep8mxuckIx+DWyo3xkPHbSstQXBo6nzGhChfCjiuODWwMe67QSgjvRCjWJHhb3FMwc1XrruI1hutV2HU3pIL/g4fxY/NuORqHBJG7wWxVunMPvzefSVqlLogr2/hDn4XLrm1HK+OGtAEMjXHGDaXFxkXDCsu/y9WITtADFC6iww1SxY+YJYeE3CdjeVZyXnatfvySjsT6g9Z4tulDkKFq1tehtvSmdf+F8X9o1/EwKvuESRB3HHc4GVSDuBeQeDLnDGRqvP4zAqYk1LFBgzgDAPwJo8SERzrTuZ+j7dMKH6yqAFYRodrAk7WkH9kCAwEAAaN/MH0wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAtBgNVHREEJjAkghBzdHMuZGV2LmNlcnQuY29tghBzdHMuZGV2LmNlcnQuY29tMB0GA1UdDgQWBBQuyHxWP3je6jGMOmOiY+hz47r36jANBgkqhkiG9w0BAQsFAAOCAQEAKEHG7Ga6nb2XiHXDc69KsIJwbO80+LE8HVJojvITILz3juN6/FmK0HmogjU6cYST7m1MyxsVhQQNwJASZ6haBNuBbNzBXfyyfb4kr62t1oDLNwhctHaHaM4sJSf/xIw+YO+Qf7BtfRAVsbM05+QXIi2LycGrzELiXu7KFM0E1+T8UOZ2Qyv7OlCb/pWkYuDgE4w97ox0MhDpvgluxZLpRanOLUCVGrfFaij7gRAhjYPUY3vAEcD8JcFBz1XijU8ozRO6FaG4qg8/JCe+VgoWsMDj3sKB9g0ob6KCyG9L2bdk99PGgvXDQvMYCpkpZzG3XsxOINPd5p0gc209ZOoxTg==',
],
alg: 'RS256',
},
],
};
const valueFalse$ = tokenValidationService.validateSignatureIdToken(
'someNOTMATCHINGIdToken',
jwtKeys,
{ configId: 'configId1' }
);
valueFalse$.subscribe((valueFalse) => {
expect(valueFalse).toEqual(false);
done();
});
});
it('should return true if valid input is provided', (done) => {
const idToken =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwiYXVkIjoibXlfY2xpZW50X2lkIiwiZXhwIjoxMzExMjgxOTcwLCJpYXQiOjEzMTEyODA5NzAsIm5hbWUiOiJKYW5lIERvZSIsImdpdmVuX25hbWUiOiJKYW5lIiwiZmFtaWx5X25hbWUiOiJEb2UiLCJiaXJ0aGRhdGUiOiIxOTkwLTEwLTMxIiwiZW1haWwiOiJqYW5lZG9lQGV4YW1wbGUuY29tIiwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vamFuZWRvZS9tZS5qcGcifQ.SY0ilps7yKYmYCc41zNOatfmAFhOtDYwuIT80qrHMl_4FEO2WFWSv-aDl4QfTSKY9A6MMP6xy0Z_8Kk7NeRwIV7FVScMLnPvVzs9pxza0e_rl6hmZLb5P5n4AEINwn46X9XmRB5W3EZO_x2LG65_g3NZFiPrzOC1Fs_6taJl7TfI8lOveYDoJyXCWYQMS3Oh5MM9S8W-Hc29_qJLH-kixm1S01qoICRPDGMRwhtAu1DHjwWQp9Ycfz6g3uyb7N1imBvI49t1CwWy02_mQ3g-7e7bOP1Ax2kgrwnJgsVBDULnyCZG9PE8T0CHZl_fErZtvbJJ0jdoZ1fyr48906am2w';
const idTokenParts = idToken.split('.');
const key = {
kty: 'RSA',
n: 'u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw',
e: 'AQAB',
alg: 'RS256',
kid: 'boop',
use: 'sig',
};
const jwtKeys = {
keys: [key],
};
spyOn(tokenHelperService, 'getHeaderFromToken').and.returnValue({
alg: 'RS256',
typ: 'JWT',
});
spyOn(tokenHelperService, 'getSigningInputFromToken').and.returnValue(
[idTokenParts[0], idTokenParts[1]].join('.')
);
spyOn(tokenHelperService, 'getSignatureFromToken').and.returnValue(
idTokenParts[2]
);
const valueTrue$ = tokenValidationService.validateSignatureIdToken(
idToken,
jwtKeys,
{ configId: 'configId1' }
);
valueTrue$.subscribe((valueTrue) => {
expect(valueTrue).toEqual(true);
done();
});
});
});
describe('validateIdTokenAtHash', () => {
it('returns true if sha is sha256 and generated hash equals atHash param', (done) => {
const accessToken = 'iGU3DhbPoDljiYtr0oepxi7zpT8BsjdU7aaXcdq-DPk';
const atHash = '-ODC_7Go_UIUTC8nP4k2cA';
const result$ = tokenValidationService.validateIdTokenAtHash(
accessToken,
atHash,
'256',
{ configId: 'configId1' }
);
result$.subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
it('returns false if sha is sha256 and generated hash does not equal atHash param', (done) => {
const accessToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const atHash = 'bad';
const result$ = tokenValidationService.validateIdTokenAtHash(
accessToken,
atHash,
'256',
{ configId: 'configId1' }
);
result$.subscribe((result) => {
expect(result).toEqual(false);
done();
});
});
it('returns true if sha is sha256 and generated hash does equal atHash param', (done) => {
const accessToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const atHash = 'good';
spyOn(jwtWindowCryptoService, 'generateAtHash').and.returnValues(
of('notEqualsGood'),
of('good')
);
const result$ = tokenValidationService.validateIdTokenAtHash(
accessToken,
atHash,
'256',
{ configId: 'configId1' }
);
result$.subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
it('returns false if sha is sha384 and generated hash does not equal atHash param', (done) => {
const accessToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const atHash = 'bad';
const result$ = tokenValidationService.validateIdTokenAtHash(
accessToken,
atHash,
'384',
{ configId: 'configId1' }
);
result$.subscribe((result) => {
expect(result).toEqual(false);
done();
});
});
it('returns false if sha is sha512 and generated hash does not equal atHash param', (done) => {
const accessToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const atHash = 'bad';
const result$ = tokenValidationService.validateIdTokenAtHash(
accessToken,
atHash,
'512',
{ configId: 'configId1' }
);
result$.subscribe((result) => {
expect(result).toEqual(false);
done();
});
});
});
describe('validateStateFromHashCallback', () => {
it('returns true when state and localstate match', () => {
const result = tokenValidationService.validateStateFromHashCallback(
'sssd',
'sssd',
{ configId: 'configId1' }
);
expect(result).toEqual(true);
});
it('returns false when state and local state do not match', () => {
const result = tokenValidationService.validateStateFromHashCallback(
'sssd',
'bad',
{ configId: 'configId1' }
);
expect(result).toEqual(false);
});
});
describe('validateIdTokenExpNotExpired', () => {
it('returns false when getTokenExpirationDate returns null', () => {
spyOn(tokenHelperService, 'getTokenExpirationDate').and.returnValue(
null as unknown as Date
);
const notExpired = tokenValidationService.validateIdTokenExpNotExpired(
'idToken',
{ configId: 'configId1' },
0
);
expect(notExpired).toEqual(false);
});
it('returns false if token is not expired', () => {
const idToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const notExpired = tokenValidationService.validateIdTokenExpNotExpired(
idToken,
{ configId: 'configId1' },
0
);
expect(notExpired).toEqual(false);
});
});
describe('validateAccessTokenNotExpired', () => {
const testCases = [
{
// Mon Jan 19 1970 10:26:50 GMT+0100,
date: new Date(1589210086),
offsetSeconds: 0,
expectedResult: false,
},
{
// Sun Nov 01 2550 00:00:00 GMT+0100
date: new Date(2550, 10),
offsetSeconds: 0,
expectedResult: true,
},
{
date: null,
offsetSeconds: 300,
expectedResult: true,
},
];
testCases.forEach(({ date, offsetSeconds, expectedResult }) => {
it(`returns ${expectedResult} if ${date} is given with an offset of ${offsetSeconds}`, () => {
const notExpired = tokenValidationService.validateAccessTokenNotExpired(
date as Date,
{ configId: 'configId1' },
offsetSeconds
);
expect(notExpired).toEqual(expectedResult);
});
});
});
describe('hasIdTokenExpired', () => {
it('returns true if token has expired', () => {
const idToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const result = tokenValidationService.hasIdTokenExpired(idToken, {
configId: 'configId1',
});
expect(result).toBe(true);
});
it('returns false if token has not expired using offset', () => {
// expires 2050-02-12T08:02:30.823Z
const tokenExpires = new Date('2050-02-12T08:02:30.823Z');
const idToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTMxMTY5NTAsImV4cCI6MjUyODI2NTc1MCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.GHxRo23NghUTTeZx6VIzTSf05JEeEn7z9YYyFLxWv6M';
spyOn(tokenHelperService, 'getTokenExpirationDate').and.returnValue(
tokenExpires
);
const result = tokenValidationService.hasIdTokenExpired(idToken, {
configId: 'configId1',
});
expect(result).toBe(false);
});
});
});

View File

@@ -0,0 +1,608 @@
import { inject, Injectable } from 'injection-js';
import { base64url } from 'rfc4648';
import { from, Observable, of } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration';
import { JwkExtractor } from '../extractors/jwk.extractor';
import { LoggerService } from '../logging/logger.service';
import { TokenHelperService } from '../utils/tokenHelper/token-helper.service';
import { JwkWindowCryptoService } from './jwk-window-crypto.service';
import { JwtWindowCryptoService } from './jwt-window-crypto.service';
import { alg2kty, getImportAlg, getVerifyAlg } from './token-validation.helper';
// http://openid.net/specs/openid-connect-implicit-1_0.html
// id_token
// id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
// MUST exactly match the value of the iss (issuer) Claim.
//
// id_token C2: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified
// by the iss (issuer) Claim as an audience.The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
// or if it contains additional audiences not trusted by the Client.
//
// id_token C3: If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
//
// id_token C4: If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
//
// id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the
// alg Header Parameter of the JOSE Header.The Client MUST use the keys provided by the Issuer.
//
// id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the OpenID Connect
// Core 1.0
// [OpenID.Core] specification.
//
// id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account
// for clock skew).
//
// id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time,
// limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific.
//
// id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one that was sent
// in the Authentication Request.The Client SHOULD check the nonce value for replay attacks.The precise method for detecting replay attacks
// is Client specific.
//
// id_token C10: If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
// The meaning and processing of acr Claim Values is out of scope for this document.
//
// id_token C11: When a max_age request is made, the Client SHOULD check the auth_time Claim value and request re- authentication
// if it determines too much time has elapsed since the last End- User authentication.
// Access Token Validation
// access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA]
// for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.
// access_token C2: Take the left- most half of the hash and base64url- encode it.
// access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash is present
// in the ID Token.
@Injectable()
export class TokenValidationService {
static refreshTokenNoncePlaceholder = '--RefreshToken--';
keyAlgorithms: string[] = [
'HS256',
'HS384',
'HS512',
'RS256',
'RS384',
'RS512',
'ES256',
'ES384',
'PS256',
'PS384',
'PS512',
];
private readonly tokenHelperService = inject(TokenHelperService);
private readonly loggerService = inject(LoggerService);
private readonly jwkExtractor = inject(JwkExtractor);
private readonly jwkWindowCryptoService = inject(JwkWindowCryptoService);
private readonly jwtWindowCryptoService = inject(JwtWindowCryptoService);
// id_token C7: The current time MUST be before the time represented by the exp Claim
// (possibly allowing for some small leeway to account for clock skew).
hasIdTokenExpired(
token: string,
configuration: OpenIdConfiguration,
offsetSeconds?: number
): boolean {
const decoded = this.tokenHelperService.getPayloadFromToken(
token,
false,
configuration
);
return !this.validateIdTokenExpNotExpired(
decoded,
configuration,
offsetSeconds
);
}
// id_token C7: The current time MUST be before the time represented by the exp Claim
// (possibly allowing for some small leeway to account for clock skew).
validateIdTokenExpNotExpired(
decodedIdToken: string,
configuration: OpenIdConfiguration,
offsetSeconds?: number
): boolean {
const tokenExpirationDate =
this.tokenHelperService.getTokenExpirationDate(decodedIdToken);
offsetSeconds = offsetSeconds || 0;
if (!tokenExpirationDate) {
return false;
}
const tokenExpirationValue = tokenExpirationDate.valueOf();
const nowWithOffset = this.calculateNowWithOffset(offsetSeconds);
const tokenNotExpired = tokenExpirationValue > nowWithOffset;
this.loggerService.logDebug(
configuration,
`Has idToken expired: ${!tokenNotExpired} --> expires in ${this.millisToMinutesAndSeconds(
tokenExpirationValue - nowWithOffset
)} , ${new Date(tokenExpirationValue).toLocaleTimeString()} > ${new Date(
nowWithOffset
).toLocaleTimeString()}`
);
return tokenNotExpired;
}
validateAccessTokenNotExpired(
accessTokenExpiresAt: Date,
configuration: OpenIdConfiguration,
offsetSeconds?: number
): boolean {
// value is optional, so if it does not exist, then it has not expired
if (!accessTokenExpiresAt) {
return true;
}
offsetSeconds = offsetSeconds || 0;
const accessTokenExpirationValue = accessTokenExpiresAt.valueOf();
const nowWithOffset = this.calculateNowWithOffset(offsetSeconds);
const tokenNotExpired = accessTokenExpirationValue > nowWithOffset;
this.loggerService.logDebug(
configuration,
`Has accessToken expired: ${!tokenNotExpired} --> expires in ${this.millisToMinutesAndSeconds(
accessTokenExpirationValue - nowWithOffset
)} , ${new Date(
accessTokenExpirationValue
).toLocaleTimeString()} > ${new Date(nowWithOffset).toLocaleTimeString()}`
);
return tokenNotExpired;
}
// iss
// REQUIRED. Issuer Identifier for the Issuer of the response.The iss value is a case-sensitive URL using the
// https scheme that contains scheme, host,
// and optionally, port number and path components and no query or fragment components.
//
// sub
// REQUIRED. Subject Identifier.Locally unique and never reassigned identifier within the Issuer for the End- User,
// which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4.
// It MUST NOT exceed 255 ASCII characters in length.The sub value is a case-sensitive string.
//
// aud
// REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an
// audience value.
// It MAY also contain identifiers for other audiences.In the general case, the aud value is an array of case-sensitive strings.
// In the common special case when there is one audience, the aud value MAY be a single case-sensitive string.
//
// exp
// REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing.
// The processing of this parameter requires that the current date/ time MUST be before the expiration date/ time listed in the value.
// Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew.
// Its value is a JSON [RFC7159] number representing the number of seconds from 1970- 01 - 01T00: 00:00Z as measured in UTC until
// the date/ time.
// See RFC 3339 [RFC3339] for details regarding date/ times in general and UTC in particular.
//
// iat
// REQUIRED. Time at which the JWT was issued. Its value is a JSON number representing the number of seconds from
// 1970- 01 - 01T00: 00: 00Z as measured
// in UTC until the date/ time.
validateRequiredIdToken(
dataIdToken: any,
configuration: OpenIdConfiguration
): boolean {
let validated = true;
if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'iss')) {
validated = false;
this.loggerService.logWarning(
configuration,
'iss is missing, this is required in the id_token'
);
}
if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'sub')) {
validated = false;
this.loggerService.logWarning(
configuration,
'sub is missing, this is required in the id_token'
);
}
if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'aud')) {
validated = false;
this.loggerService.logWarning(
configuration,
'aud is missing, this is required in the id_token'
);
}
if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'exp')) {
validated = false;
this.loggerService.logWarning(
configuration,
'exp is missing, this is required in the id_token'
);
}
if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'iat')) {
validated = false;
this.loggerService.logWarning(
configuration,
'iat is missing, this is required in the id_token'
);
}
return validated;
}
// id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time,
// limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific.
validateIdTokenIatMaxOffset(
dataIdToken: any,
maxOffsetAllowedInSeconds: number,
disableIatOffsetValidation: boolean,
configuration: OpenIdConfiguration
): boolean {
if (disableIatOffsetValidation) {
return true;
}
if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'iat')) {
return false;
}
const dateTimeIatIdToken = new Date(0); // The 0 here is the key, which sets the date to the epoch
dateTimeIatIdToken.setUTCSeconds(dataIdToken.iat);
maxOffsetAllowedInSeconds = maxOffsetAllowedInSeconds || 0;
const nowInUtc = new Date(new Date().toUTCString());
const diff = nowInUtc.valueOf() - dateTimeIatIdToken.valueOf();
const maxOffsetAllowedInMilliseconds = maxOffsetAllowedInSeconds * 1000;
this.loggerService.logDebug(
configuration,
`validate id token iat max offset ${diff} < ${maxOffsetAllowedInMilliseconds}`
);
if (diff > 0) {
return diff < maxOffsetAllowedInMilliseconds;
}
return -diff < maxOffsetAllowedInMilliseconds;
}
// id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one
// that was sent in the Authentication Request.The Client SHOULD check the nonce value for replay attacks.
// The precise method for detecting replay attacks is Client specific.
// However the nonce claim SHOULD not be present for the refresh_token grant type
// https://bitbucket.org/openid/connect/issues/1025/ambiguity-with-how-nonce-is-handled-on
// The current spec is ambiguous and KeyCloak does send it.
validateIdTokenNonce(
dataIdToken: any,
localNonce: any,
ignoreNonceAfterRefresh: boolean,
configuration: OpenIdConfiguration
): boolean {
const isFromRefreshToken =
(dataIdToken.nonce === undefined || ignoreNonceAfterRefresh) &&
localNonce === TokenValidationService.refreshTokenNoncePlaceholder;
if (!isFromRefreshToken && dataIdToken.nonce !== localNonce) {
this.loggerService.logDebug(
configuration,
'Validate_id_token_nonce failed, dataIdToken.nonce: ' +
dataIdToken.nonce +
' local_nonce:' +
localNonce
);
return false;
}
return true;
}
// id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
// MUST exactly match the value of the iss (issuer) Claim.
validateIdTokenIss(
dataIdToken: any,
authWellKnownEndpointsIssuer: any,
configuration: OpenIdConfiguration
): boolean {
if (
(dataIdToken.iss as string) !== (authWellKnownEndpointsIssuer as string)
) {
this.loggerService.logDebug(
configuration,
'Validate_id_token_iss failed, dataIdToken.iss: ' +
dataIdToken.iss +
' authWellKnownEndpoints issuer:' +
authWellKnownEndpointsIssuer
);
return false;
}
return true;
}
// id_token C2: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified
// by the iss (issuer) Claim as an audience.
// The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences
// not trusted by the Client.
validateIdTokenAud(
dataIdToken: any,
aud: string | undefined,
configuration: OpenIdConfiguration
): boolean {
if (Array.isArray(dataIdToken.aud)) {
const result = dataIdToken.aud.includes(aud);
if (!result) {
this.loggerService.logDebug(
configuration,
'Validate_id_token_aud array failed, dataIdToken.aud: ' +
dataIdToken.aud +
' client_id:' +
aud
);
return false;
}
return true;
} else if (dataIdToken.aud !== aud) {
this.loggerService.logDebug(
configuration,
'Validate_id_token_aud failed, dataIdToken.aud: ' +
dataIdToken.aud +
' client_id:' +
aud
);
return false;
}
return true;
}
validateIdTokenAzpExistsIfMoreThanOneAud(dataIdToken: any): boolean {
if (!dataIdToken) {
return false;
}
return !(
Array.isArray(dataIdToken.aud) &&
dataIdToken.aud.length > 1 &&
!dataIdToken.azp
);
}
// If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
validateIdTokenAzpValid(
dataIdToken: any,
clientId: string | undefined
): boolean {
if (!dataIdToken?.azp) {
return true;
}
return dataIdToken.azp === clientId;
}
validateStateFromHashCallback(
state: any,
localState: any,
configuration: OpenIdConfiguration
): boolean {
if ((state as string) !== (localState as string)) {
this.loggerService.logDebug(
configuration,
'ValidateStateFromHashCallback failed, state: ' +
state +
' local_state:' +
localState
);
return false;
}
return true;
}
// id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the alg
// Header Parameter of the JOSE Header.The Client MUST use the keys provided by the Issuer.
// id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the
// OpenID Connect Core 1.0 [OpenID.Core] specification.
validateSignatureIdToken(
idToken: string,
jwtkeys: any,
configuration: OpenIdConfiguration
): Observable<boolean> {
if (!idToken) {
return of(true);
}
if (!jwtkeys || !jwtkeys.keys) {
return of(false);
}
const headerData = this.tokenHelperService.getHeaderFromToken(
idToken,
false,
configuration
);
if (
Object.keys(headerData).length === 0 &&
headerData.constructor === Object
) {
this.loggerService.logWarning(
configuration,
'id token has no header data'
);
return of(false);
}
const kid: string = headerData.kid;
const alg: string = headerData.alg;
const keys: JsonWebKey[] = jwtkeys.keys;
let foundKeys: JsonWebKey[];
let key: JsonWebKey;
if (!this.keyAlgorithms.includes(alg)) {
this.loggerService.logWarning(configuration, 'alg not supported', alg);
return of(false);
}
const kty = alg2kty(alg);
const use = 'sig';
try {
foundKeys = kid
? this.jwkExtractor.extractJwk(keys, { kid, kty, use }, false)
: this.jwkExtractor.extractJwk(keys, { kty, use }, false);
if (foundKeys.length === 0) {
foundKeys = kid
? this.jwkExtractor.extractJwk(keys, { kid, kty })
: this.jwkExtractor.extractJwk(keys, { kty });
}
key = foundKeys[0];
} catch (e: any) {
this.loggerService.logError(configuration, e);
return of(false);
}
const algorithm = getImportAlg(alg);
const signingInput = this.tokenHelperService.getSigningInputFromToken(
idToken,
true,
configuration
);
const rawSignature = this.tokenHelperService.getSignatureFromToken(
idToken,
true,
configuration
);
return from(
this.jwkWindowCryptoService.importVerificationKey(key, algorithm)
).pipe(
mergeMap((cryptoKey: CryptoKey) => {
const signature: Uint8Array = base64url.parse(rawSignature, {
loose: true,
});
const verifyAlgorithm = getVerifyAlg(alg);
return from(
this.jwkWindowCryptoService.verifyKey(
verifyAlgorithm,
cryptoKey,
signature,
signingInput
)
);
}),
tap((isValid: boolean) => {
if (!isValid) {
this.loggerService.logWarning(
configuration,
'incorrect Signature, validation failed for id_token'
);
}
})
);
}
// Accepts ID Token without 'kid' claim in JOSE header if only one JWK supplied in 'jwks_url'
//// private validate_no_kid_in_header_only_one_allowed_in_jwtkeys(header_data: any, jwtkeys: any): boolean {
//// this.oidcSecurityCommon.logDebug('amount of jwtkeys.keys: ' + jwtkeys.keys.length);
//// if (!header_data.hasOwnProperty('kid')) {
//// // no kid defined in Jose header
//// if (jwtkeys.keys.length != 1) {
//// this.oidcSecurityCommon.logDebug('jwtkeys.keys.length != 1 and no kid in header');
//// return false;
//// }
//// }
//// return true;
//// }
// Access Token Validation
// access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA]
// for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.
// access_token C2: Take the left- most half of the hash and base64url- encode it.
// access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash
// is present in the ID Token.
validateIdTokenAtHash(
accessToken: string,
atHash: string,
idTokenAlg: string,
configuration: OpenIdConfiguration
): Observable<boolean> {
this.loggerService.logDebug(
configuration,
'at_hash from the server:' + atHash
);
// 'sha256' 'sha384' 'sha512'
let sha = 'SHA-256';
if (idTokenAlg.includes('384')) {
sha = 'SHA-384';
} else if (idTokenAlg.includes('512')) {
sha = 'SHA-512';
}
return this.jwtWindowCryptoService
.generateAtHash('' + accessToken, sha)
.pipe(
mergeMap((hash: string) => {
this.loggerService.logDebug(
configuration,
'at_hash client validation not decoded:' + hash
);
if (hash === atHash) {
return of(true); // isValid;
} else {
return this.jwtWindowCryptoService
.generateAtHash('' + decodeURIComponent(accessToken), sha)
.pipe(
map((newHash: string) => {
this.loggerService.logDebug(
configuration,
'-gen access--' + hash
);
return newHash === atHash;
})
);
}
})
);
}
private millisToMinutesAndSeconds(millis: number): string {
const minutes = Math.floor(millis / 60000);
const seconds = ((millis % 60000) / 1000).toFixed(0);
return minutes + ':' + (+seconds < 10 ? '0' : '') + seconds;
}
private calculateNowWithOffset(offsetSeconds: number): number {
return new Date(new Date().toUTCString()).valueOf() + offsetSeconds * 1000;
}
}

View File

@@ -0,0 +1,18 @@
export enum ValidationResult {
NotSet = 'NotSet',
StatesDoNotMatch = 'StatesDoNotMatch',
SignatureFailed = 'SignatureFailed',
IncorrectNonce = 'IncorrectNonce',
RequiredPropertyMissing = 'RequiredPropertyMissing',
MaxOffsetExpired = 'MaxOffsetExpired',
IssDoesNotMatchIssuer = 'IssDoesNotMatchIssuer',
NoAuthWellKnownEndPoints = 'NoAuthWellKnownEndPoints',
IncorrectAud = 'IncorrectAud',
IncorrectIdTokenClaimsAfterRefresh = 'IncorrectIdTokenClaimsAfterRefresh',
IncorrectAzp = 'IncorrectAzp',
TokenExpired = 'TokenExpired',
IncorrectAtHash = 'IncorrectAtHash',
Ok = 'Ok',
LoginRequired = 'LoginRequired',
SecureTokenServerError = 'SecureTokenServerError',
}