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
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:
92
src/validation/jwk-window-crypto.service.spec.ts
Normal file
92
src/validation/jwk-window-crypto.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
38
src/validation/jwk-window-crypto.service.ts
Normal file
38
src/validation/jwk-window-crypto.service.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/validation/jwt-window-crypto.service.spec.ts
Normal file
35
src/validation/jwt-window-crypto.service.spec.ts
Normal 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);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
63
src/validation/jwt-window-crypto.service.ts
Normal file
63
src/validation/jwt-window-crypto.service.ts
Normal 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
13
src/validation/jwtkeys.ts
Normal 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[];
|
||||
}
|
||||
13
src/validation/state-validation-result.ts
Normal file
13
src/validation/state-validation-result.ts
Normal 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
|
||||
) {}
|
||||
}
|
||||
2155
src/validation/state-validation.service.spec.ts
Normal file
2155
src/validation/state-validation.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
539
src/validation/state-validation.service.ts
Normal file
539
src/validation/state-validation.service.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
159
src/validation/token-validation.helper.spec.ts
Normal file
159
src/validation/token-validation.helper.spec.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
82
src/validation/token-validation.helper.ts
Normal file
82
src/validation/token-validation.helper.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
862
src/validation/token-validation.service.spec.ts
Normal file
862
src/validation/token-validation.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
608
src/validation/token-validation.service.ts
Normal file
608
src/validation/token-validation.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
18
src/validation/validation-result.ts
Normal file
18
src/validation/validation-result.ts
Normal 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',
|
||||
}
|
||||
Reference in New Issue
Block a user