mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-02 17:53:30 +00:00
AAE-30881 Remove jsrassign (#11863)
* [AAE-30881] - Remove jsrassign as dep * [AAE-30881] - Remove jsrassign as dep * [AAE-30881] - upgrade oidc package * [AAE-30881] - added unit tests * [AAE-30881] - fixing security issue * [AAE-30881] - Fixed sonarcloud issue * [AAE-30881] - Fixed sonarcloud issue
This commit is contained in:
@@ -38,7 +38,6 @@
|
||||
"allowedNonPeerDependencies": [
|
||||
"cropperjs",
|
||||
"angular-oauth2-oidc",
|
||||
"angular-oauth2-oidc-jwks",
|
||||
"date-fns",
|
||||
"rxjs"
|
||||
]
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cropperjs": "^1.6.2",
|
||||
"angular-oauth2-oidc": "17.0.2",
|
||||
"angular-oauth2-oidc-jwks": "17.0.2",
|
||||
"angular-oauth2-oidc": "19.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"rxjs": "7.8.2"
|
||||
},
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from './oidc-auth.guard';
|
||||
export * from './redirect-auth.service';
|
||||
export * from './view/authentication-confirmation/authentication-confirmation.component';
|
||||
export * from './oidc-authentication.service';
|
||||
export * from './web-crypto-jwks-validation-handler';
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
OAuthSuccessEvent,
|
||||
OAuthLogger
|
||||
} from 'angular-oauth2-oidc';
|
||||
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
||||
import { WebCryptoJwksValidationHandler } from './web-crypto-jwks-validation-handler';
|
||||
import { from, Observable, race, ReplaySubject } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
import { AuthService } from './auth.service';
|
||||
@@ -338,7 +338,7 @@ export class RedirectAuthService extends AuthService {
|
||||
|
||||
private configureAuth(config: AuthConfig): Promise<boolean> {
|
||||
this.oauthService.configure(config);
|
||||
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
|
||||
this.oauthService.tokenValidationHandler = new WebCryptoJwksValidationHandler();
|
||||
|
||||
if (config.sessionChecksEnabled) {
|
||||
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ValidationParams } from 'angular-oauth2-oidc';
|
||||
import { WebCryptoJwksValidationHandler } from './web-crypto-jwks-validation-handler';
|
||||
|
||||
/**
|
||||
* Encodes data to base64url format
|
||||
*
|
||||
* @param data - input data as ArrayBuffer or string
|
||||
* @returns base64url encoded string
|
||||
*/
|
||||
function base64UrlEncode(data: ArrayBuffer | string): string {
|
||||
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data);
|
||||
let binary = '';
|
||||
for (const byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
interface JwksKey extends JsonWebKey {
|
||||
kid?: string;
|
||||
use?: string;
|
||||
}
|
||||
|
||||
interface TestKeyPair {
|
||||
privateKey: CryptoKey;
|
||||
publicJwk: JwksKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an RSA key pair for testing
|
||||
*
|
||||
* @param kid - optional key ID to assign
|
||||
* @returns test key pair with private key and public JWK
|
||||
*/
|
||||
async function generateRsaKeyPair(kid?: string): Promise<TestKeyPair> {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
);
|
||||
const publicJwk: JwksKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
publicJwk.use = 'sig';
|
||||
if (kid) {
|
||||
publicJwk.kid = kid;
|
||||
}
|
||||
return { privateKey: keyPair.privateKey, publicJwk };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an EC key pair for testing
|
||||
*
|
||||
* @param namedCurve - elliptic curve name (e.g. P-256)
|
||||
* @param kid - optional key ID to assign
|
||||
* @returns test key pair with private key and public JWK
|
||||
*/
|
||||
async function generateEcKeyPair(namedCurve: string, kid?: string): Promise<TestKeyPair> {
|
||||
const keyPair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve }, true, ['sign', 'verify']);
|
||||
const publicJwk: JwksKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
publicJwk.use = 'sig';
|
||||
if (kid) {
|
||||
publicJwk.kid = kid;
|
||||
}
|
||||
return { privateKey: keyPair.privateKey, publicJwk };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signed JWT for testing
|
||||
*
|
||||
* @param header - JWT header object
|
||||
* @param payload - JWT payload object
|
||||
* @param privateKey - key to sign with
|
||||
* @param algorithm - signing algorithm parameters
|
||||
* @returns signed JWT string
|
||||
*/
|
||||
async function createSignedJwt(
|
||||
header: object,
|
||||
payload: object,
|
||||
privateKey: CryptoKey,
|
||||
algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams
|
||||
): Promise<string> {
|
||||
const headerB64 = base64UrlEncode(JSON.stringify(header));
|
||||
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
|
||||
const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
||||
const signature = await crypto.subtle.sign(algorithm, privateKey, signingInput);
|
||||
const signatureB64 = base64UrlEncode(signature);
|
||||
return `${headerB64}.${payloadB64}.${signatureB64}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a ValidationParams object with defaults for testing
|
||||
*
|
||||
* @param overrides - partial params to override defaults
|
||||
* @returns complete ValidationParams
|
||||
*/
|
||||
function buildValidationParams(overrides: Partial<ValidationParams>): ValidationParams {
|
||||
return {
|
||||
idToken: '',
|
||||
accessToken: '',
|
||||
idTokenHeader: {},
|
||||
idTokenClaims: {},
|
||||
jwks: {},
|
||||
loadKeys: undefined,
|
||||
...overrides
|
||||
} as ValidationParams;
|
||||
}
|
||||
|
||||
describe('WebCryptoJwksValidationHandler', () => {
|
||||
let handler: WebCryptoJwksValidationHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new WebCryptoJwksValidationHandler();
|
||||
});
|
||||
|
||||
describe('parameter validation', () => {
|
||||
it('should reject when idToken is missing', async () => {
|
||||
const params = buildValidationParams({ idToken: '' });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Parameter idToken expected!');
|
||||
});
|
||||
|
||||
it('should reject when token has less than 3 parts', async () => {
|
||||
const params = buildValidationParams({ idToken: 'part1.part2' });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError(
|
||||
'Invalid JWT format: token must have exactly 3 non-empty parts separated by dots.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject when token has more than 3 parts', async () => {
|
||||
const params = buildValidationParams({ idToken: 'a.b.c.d' });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError(
|
||||
'Invalid JWT format: token must have exactly 3 non-empty parts separated by dots.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject when token has empty parts', async () => {
|
||||
const params = buildValidationParams({ idToken: 'a..c' });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError(
|
||||
'Invalid JWT format: token must have exactly 3 non-empty parts separated by dots.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject when idTokenHeader is missing', async () => {
|
||||
const params = buildValidationParams({ idToken: 'a.b.c', idTokenHeader: null });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Parameter idTokenHeader expected.');
|
||||
});
|
||||
|
||||
it('should reject when jwks is missing', async () => {
|
||||
const params = buildValidationParams({ idToken: 'a.b.c', idTokenHeader: { alg: 'RS256' }, jwks: null });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Parameter jwks expected!');
|
||||
});
|
||||
|
||||
it('should reject when jwks keys array is empty', async () => {
|
||||
const params = buildValidationParams({ idToken: 'a.b.c', idTokenHeader: { alg: 'RS256' }, jwks: { keys: [] } });
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Array keys in jwks missing!');
|
||||
});
|
||||
|
||||
it('should reject when algorithm is not supported', async () => {
|
||||
const { publicJwk } = await generateRsaKeyPair('test-kid');
|
||||
const params = buildValidationParams({
|
||||
idToken: 'a.b.c',
|
||||
idTokenHeader: { alg: 'none', kid: 'test-kid' },
|
||||
jwks: { keys: [publicJwk] }
|
||||
});
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Algorithm not supported: none');
|
||||
});
|
||||
|
||||
it('should reject when algorithm is missing and no kid is present', async () => {
|
||||
const { publicJwk } = await generateRsaKeyPair();
|
||||
const params = buildValidationParams({
|
||||
idToken: 'a.b.c',
|
||||
idTokenHeader: {},
|
||||
jwks: { keys: [publicJwk] }
|
||||
});
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Algorithm not supported: <none>');
|
||||
});
|
||||
|
||||
it('should reject when algorithm is unsupported and no kid is present', async () => {
|
||||
const { publicJwk } = await generateRsaKeyPair();
|
||||
const params = buildValidationParams({
|
||||
idToken: 'a.b.c',
|
||||
idTokenHeader: { alg: 'HS256' },
|
||||
jwks: { keys: [publicJwk] }
|
||||
});
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Algorithm not supported: HS256');
|
||||
});
|
||||
});
|
||||
|
||||
describe('key selection by kid', () => {
|
||||
it('should select the key matching the kid in the token header', async () => {
|
||||
const key1 = await generateRsaKeyPair('key-1');
|
||||
const key2 = await generateRsaKeyPair('key-2');
|
||||
|
||||
const header = { alg: 'RS256', kid: 'key-2' };
|
||||
const payload = { sub: '123' };
|
||||
const idToken = await createSignedJwt(header, payload, key2.privateKey, { name: 'RSASSA-PKCS1-v1_5' });
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [key1.publicJwk, key2.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should reject when kid does not match any key after retry', async () => {
|
||||
const key1 = await generateRsaKeyPair('key-1');
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken: 'a.b.c',
|
||||
idTokenHeader: { alg: 'RS256', kid: 'unknown-kid' },
|
||||
jwks: { keys: [key1.publicJwk] },
|
||||
loadKeys: () => Promise.resolve({ keys: [key1.publicJwk] })
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError(/expected key not found in property jwks/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('key selection without kid', () => {
|
||||
it('should select the single matching key by kty and use', async () => {
|
||||
const rsaKey = await generateRsaKeyPair();
|
||||
const header = { alg: 'RS256' };
|
||||
const payload = { sub: '456' };
|
||||
const idToken = await createSignedJwt(header, payload, rsaKey.privateKey, { name: 'RSASSA-PKCS1-v1_5' });
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [rsaKey.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should reject when multiple keys match without kid', async () => {
|
||||
const key1 = await generateRsaKeyPair();
|
||||
const key2 = await generateRsaKeyPair();
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken: 'a.b.c',
|
||||
idTokenHeader: { alg: 'RS256' },
|
||||
jwks: { keys: [key1.publicJwk, key2.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError(
|
||||
'More than one matching key found. Please specify a kid in the id_token header.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject when no key matches after retry', async () => {
|
||||
const ecKey = await generateEcKeyPair('P-256');
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken: 'a.b.c',
|
||||
idTokenHeader: { alg: 'RS256' },
|
||||
jwks: { keys: [ecKey.publicJwk] },
|
||||
loadKeys: () => Promise.resolve({ keys: [ecKey.publicJwk] })
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('No matching key found.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadKeys retry', () => {
|
||||
it('should call loadKeys and retry when key is not found on first attempt', async () => {
|
||||
const rsaKey = await generateRsaKeyPair('delayed-key');
|
||||
const header = { alg: 'RS256', kid: 'delayed-key' };
|
||||
const payload = { sub: '789' };
|
||||
const idToken = await createSignedJwt(header, payload, rsaKey.privateKey, { name: 'RSASSA-PKCS1-v1_5' });
|
||||
|
||||
const otherKey = await generateRsaKeyPair('other-key');
|
||||
const emptyJwks = { keys: [otherKey.publicJwk] };
|
||||
const loadedJwks = { keys: [rsaKey.publicJwk] };
|
||||
const loadKeysSpy = jasmine.createSpy('loadKeys').and.returnValue(Promise.resolve(loadedJwks));
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: emptyJwks,
|
||||
loadKeys: loadKeysSpy
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeResolved();
|
||||
expect(loadKeysSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RS256 signature verification', () => {
|
||||
it('should resolve for a valid RS256 signature', async () => {
|
||||
const rsaKey = await generateRsaKeyPair('rsa-kid');
|
||||
const header = { alg: 'RS256', kid: 'rsa-kid' };
|
||||
const payload = { sub: 'user1', iat: Math.floor(Date.now() / 1000) };
|
||||
const idToken = await createSignedJwt(header, payload, rsaKey.privateKey, { name: 'RSASSA-PKCS1-v1_5' });
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [rsaKey.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should reject for a tampered RS256 token', async () => {
|
||||
const rsaKey = await generateRsaKeyPair('rsa-kid');
|
||||
const header = { alg: 'RS256', kid: 'rsa-kid' };
|
||||
const payload = { sub: 'user1' };
|
||||
const idToken = await createSignedJwt(header, payload, rsaKey.privateKey, { name: 'RSASSA-PKCS1-v1_5' });
|
||||
|
||||
const parts = idToken.split('.');
|
||||
const tamperedPayload = base64UrlEncode(JSON.stringify({ sub: 'attacker' }));
|
||||
const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken: tamperedToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [rsaKey.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Signature not valid');
|
||||
});
|
||||
|
||||
it('should reject when signed with a different key', async () => {
|
||||
const signingKey = await generateRsaKeyPair('signing-key');
|
||||
const verifyKey = await generateRsaKeyPair('verify-key');
|
||||
const header = { alg: 'RS256', kid: 'verify-key' };
|
||||
const payload = { sub: 'user1' };
|
||||
const idToken = await createSignedJwt(header, payload, signingKey.privateKey, { name: 'RSASSA-PKCS1-v1_5' });
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [verifyKey.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Signature not valid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ES256 signature verification', () => {
|
||||
it('should resolve for a valid ES256 signature', async () => {
|
||||
const ecKey = await generateEcKeyPair('P-256', 'ec-kid');
|
||||
const header = { alg: 'ES256', kid: 'ec-kid' };
|
||||
const payload = { sub: 'user2' };
|
||||
const idToken = await createSignedJwt(header, payload, ecKey.privateKey, { name: 'ECDSA', hash: 'SHA-256' });
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [ecKey.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should reject for a tampered ES256 token', async () => {
|
||||
const ecKey = await generateEcKeyPair('P-256', 'ec-kid');
|
||||
const header = { alg: 'ES256', kid: 'ec-kid' };
|
||||
const payload = { sub: 'user2' };
|
||||
const idToken = await createSignedJwt(header, payload, ecKey.privateKey, { name: 'ECDSA', hash: 'SHA-256' });
|
||||
|
||||
const parts = idToken.split('.');
|
||||
const tamperedPayload = base64UrlEncode(JSON.stringify({ sub: 'attacker' }));
|
||||
const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken: tamperedToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [ecKey.publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeRejectedWithError('Signature not valid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PS256 signature verification', () => {
|
||||
it('should resolve for a valid PS256 signature', async () => {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'RSA-PSS', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
);
|
||||
const publicJwk: JwksKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
publicJwk.use = 'sig';
|
||||
publicJwk.kid = 'ps-kid';
|
||||
|
||||
const header = { alg: 'PS256', kid: 'ps-kid' };
|
||||
const payload = { sub: 'user3' };
|
||||
const idToken = await createSignedJwt(header, payload, keyPair.privateKey, { name: 'RSA-PSS', saltLength: 32 });
|
||||
|
||||
const params = buildValidationParams({
|
||||
idToken,
|
||||
idTokenHeader: header,
|
||||
jwks: { keys: [publicJwk] }
|
||||
});
|
||||
|
||||
await expectAsync(handler.validateSignature(params)).toBeResolved();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AbstractValidationHandler, ValidationParams } from 'angular-oauth2-oidc';
|
||||
|
||||
interface JwksKey extends JsonWebKey {
|
||||
kid?: string;
|
||||
use?: string;
|
||||
}
|
||||
|
||||
export class WebCryptoJwksValidationHandler extends AbstractValidationHandler {
|
||||
allowedAlgorithms: string[] = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'PS256', 'PS384', 'PS512'];
|
||||
|
||||
gracePeriodInSec = 600;
|
||||
|
||||
async validateSignature(params: ValidationParams, retry = false): Promise<any> {
|
||||
this.validateParams(params);
|
||||
|
||||
const keyId: string = params.idTokenHeader['kid'];
|
||||
const jwksKeys: JwksKey[] = params.jwks['keys'];
|
||||
const algorithm: string = params.idTokenHeader['alg'];
|
||||
|
||||
if (!algorithm || !this.allowedAlgorithms.includes(algorithm)) {
|
||||
throw new Error('Algorithm not supported: ' + (algorithm || '<none>'));
|
||||
}
|
||||
|
||||
const matchedKey = this.findMatchingKey(jwksKeys, keyId, algorithm);
|
||||
|
||||
if (!matchedKey && !retry && params.loadKeys) {
|
||||
params.jwks = await params.loadKeys();
|
||||
return this.validateSignature(params, true);
|
||||
}
|
||||
|
||||
if (!matchedKey && retry) {
|
||||
throw this.buildKeyNotFoundError(keyId);
|
||||
}
|
||||
|
||||
const cryptoKey = await this.importKey(matchedKey, algorithm);
|
||||
const isValid = await this.verifySignature(params.idToken, cryptoKey, algorithm);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Signature not valid');
|
||||
}
|
||||
}
|
||||
|
||||
private validateParams(params: ValidationParams): void {
|
||||
if (!params.idToken) {
|
||||
throw new Error('Parameter idToken expected!');
|
||||
}
|
||||
|
||||
const tokenParts = params.idToken.split('.');
|
||||
if (tokenParts.length !== 3 || tokenParts.some((part) => !part)) {
|
||||
throw new Error('Invalid JWT format: token must have exactly 3 non-empty parts separated by dots.');
|
||||
}
|
||||
|
||||
if (!params.idTokenHeader) {
|
||||
throw new Error('Parameter idTokenHeader expected.');
|
||||
}
|
||||
if (!params.jwks) {
|
||||
throw new Error('Parameter jwks expected!');
|
||||
}
|
||||
if (!params.jwks['keys'] || !Array.isArray(params.jwks['keys']) || params.jwks['keys'].length === 0) {
|
||||
throw new Error('Array keys in jwks missing!');
|
||||
}
|
||||
}
|
||||
|
||||
private findMatchingKey(jwksKeys: JwksKey[], keyId: string, algorithm: string): JwksKey | undefined {
|
||||
if (keyId) {
|
||||
return jwksKeys.find((jwk) => jwk.kid === keyId);
|
||||
}
|
||||
|
||||
const keyType = this.algorithmToKeyType(algorithm);
|
||||
const matchingKeys = jwksKeys.filter((jwk) => jwk.kty === keyType && jwk.use === 'sig');
|
||||
|
||||
if (matchingKeys.length > 1) {
|
||||
throw new Error('More than one matching key found. Please specify a kid in the id_token header.');
|
||||
}
|
||||
|
||||
return matchingKeys[0];
|
||||
}
|
||||
|
||||
private buildKeyNotFoundError(keyId: string): Error {
|
||||
if (!keyId) {
|
||||
return new Error('No matching key found.');
|
||||
}
|
||||
return new Error(
|
||||
'expected key not found in property jwks. ' +
|
||||
'This property is most likely loaded with the ' +
|
||||
'discovery document. ' +
|
||||
'Expected key id (kid): ' +
|
||||
keyId
|
||||
);
|
||||
}
|
||||
|
||||
private async importKey(jsonWebKey: JwksKey, algorithmName: string): Promise<CryptoKey> {
|
||||
const importAlgorithm = this.getImportAlgorithm(algorithmName);
|
||||
return crypto.subtle.importKey('jwk', jsonWebKey, importAlgorithm, false, ['verify']);
|
||||
}
|
||||
|
||||
private async verifySignature(idToken: string, cryptoKey: CryptoKey, algorithmName: string): Promise<boolean> {
|
||||
const tokenParts = idToken.split('.');
|
||||
const headerAndPayload = new TextEncoder().encode(tokenParts[0] + '.' + tokenParts[1]);
|
||||
const signature = this.base64UrlDecode(tokenParts[2]);
|
||||
const verifyAlgorithm = this.getVerifyAlgorithm(algorithmName);
|
||||
return crypto.subtle.verify(verifyAlgorithm, cryptoKey, signature, headerAndPayload);
|
||||
}
|
||||
|
||||
private getImportAlgorithm(algorithmName: string): RsaHashedImportParams | EcKeyImportParams {
|
||||
switch (algorithmName) {
|
||||
case 'RS256':
|
||||
return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
|
||||
case 'RS384':
|
||||
return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-384' };
|
||||
case 'RS512':
|
||||
return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-512' };
|
||||
case 'ES256':
|
||||
return { name: 'ECDSA', namedCurve: 'P-256' };
|
||||
case 'ES384':
|
||||
return { name: 'ECDSA', namedCurve: 'P-384' };
|
||||
case 'PS256':
|
||||
return { name: 'RSA-PSS', hash: 'SHA-256' };
|
||||
case 'PS384':
|
||||
return { name: 'RSA-PSS', hash: 'SHA-384' };
|
||||
case 'PS512':
|
||||
return { name: 'RSA-PSS', hash: 'SHA-512' };
|
||||
default:
|
||||
throw new Error('Unsupported algorithm: ' + algorithmName);
|
||||
}
|
||||
}
|
||||
|
||||
private getVerifyAlgorithm(algorithmName: string): AlgorithmIdentifier | RsaPssParams | EcdsaParams {
|
||||
switch (algorithmName) {
|
||||
case 'RS256':
|
||||
case 'RS384':
|
||||
case 'RS512':
|
||||
return { name: 'RSASSA-PKCS1-v1_5' };
|
||||
case 'ES256':
|
||||
return { name: 'ECDSA', hash: 'SHA-256' };
|
||||
case 'ES384':
|
||||
return { name: 'ECDSA', hash: 'SHA-384' };
|
||||
case 'PS256':
|
||||
return { name: 'RSA-PSS', saltLength: 32 };
|
||||
case 'PS384':
|
||||
return { name: 'RSA-PSS', saltLength: 48 };
|
||||
case 'PS512':
|
||||
return { name: 'RSA-PSS', saltLength: 64 };
|
||||
default:
|
||||
throw new Error('Unsupported algorithm: ' + algorithmName);
|
||||
}
|
||||
}
|
||||
|
||||
private base64UrlDecode(input: string): ArrayBuffer {
|
||||
let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (base64.length % 4 !== 0) {
|
||||
base64 += '=';
|
||||
}
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index++) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
protected async calcHash(valueToHash: string, algorithm: string): Promise<string> {
|
||||
const msgBuffer = new TextEncoder().encode(valueToHash);
|
||||
const hashBuffer = await crypto.subtle.digest(algorithm, msgBuffer);
|
||||
const hashArray = new Uint8Array(hashBuffer);
|
||||
let result = '';
|
||||
for (const byte of hashArray) {
|
||||
result += String.fromCharCode(byte);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private algorithmToKeyType(algorithmName: string): string {
|
||||
switch (algorithmName.charAt(0)) {
|
||||
case 'R':
|
||||
return 'RSA';
|
||||
case 'E':
|
||||
return 'EC';
|
||||
case 'P':
|
||||
return 'RSA';
|
||||
default:
|
||||
throw new Error('Cannot infer key type from algorithm: ' + algorithmName);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+6
-26
@@ -24,8 +24,7 @@
|
||||
"@cspell/eslint-plugin": "9.4.0",
|
||||
"@mat-datetimepicker/core": "15.0.2",
|
||||
"@ngx-translate/core": "^17.0.0",
|
||||
"angular-oauth2-oidc": "17.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^17.0.2",
|
||||
"angular-oauth2-oidc": "19.0.0",
|
||||
"apollo-angular": "10.0.3",
|
||||
"chart.js": "4.4.4",
|
||||
"cropperjs": "1.6.2",
|
||||
@@ -13118,26 +13117,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/angular-oauth2-oidc": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-17.0.2.tgz",
|
||||
"integrity": "sha512-zYgeLmAnu1g8XAYZK+csAsCQBDhgp9ffBv/eArEnujGxNPTeK00bREHWObtehflpQdSn+k9rY2D15ChCSydyVw==",
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-19.0.0.tgz",
|
||||
"integrity": "sha512-EogHyF7MpCJSjSKIyVmdB8pJu7dU5Ilj9VNVSnFbLng4F77PIlaE4egwKUlUvk0i4ZvmO9rLXNQCm05R7Tyhcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=14.0.0",
|
||||
"@angular/core": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/angular-oauth2-oidc-jwks": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-oauth2-oidc-jwks/-/angular-oauth2-oidc-jwks-17.0.2.tgz",
|
||||
"integrity": "sha512-zG0udq9VihQdCKfGjhUfrIg35TbxU34tGfOG/pddxKwJkodMAFI34cNoZoVyZ53hPgeZSDPra2rYyixkH7bkKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsrsasign": "^11.0.0",
|
||||
"tslib": "^2.5.2"
|
||||
"@angular/common": ">=19.0.0",
|
||||
"@angular/core": ">=19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
@@ -25470,15 +25459,6 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsrsasign": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.1.tgz",
|
||||
"integrity": "sha512-6w95OOXH8DNeGxakqLndBEqqwQ6A70zGaky1oxfg8WVLWOnghTfJsc5Tknx+Z88MHSb1bGLcqQHImOF8Lk22XA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/kjur/jsrsasign#donations"
|
||||
}
|
||||
},
|
||||
"node_modules/karma": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
|
||||
|
||||
+1
-2
@@ -53,8 +53,7 @@
|
||||
"@cspell/eslint-plugin": "9.4.0",
|
||||
"@mat-datetimepicker/core": "15.0.2",
|
||||
"@ngx-translate/core": "^17.0.0",
|
||||
"angular-oauth2-oidc": "17.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^17.0.2",
|
||||
"angular-oauth2-oidc": "19.0.0",
|
||||
"apollo-angular": "10.0.3",
|
||||
"chart.js": "4.4.4",
|
||||
"cropperjs": "1.6.2",
|
||||
|
||||
Reference in New Issue
Block a user