diff --git a/lib/core/ng-package.json b/lib/core/ng-package.json index 761244a6c4..1a536e6a39 100644 --- a/lib/core/ng-package.json +++ b/lib/core/ng-package.json @@ -38,7 +38,6 @@ "allowedNonPeerDependencies": [ "cropperjs", "angular-oauth2-oidc", - "angular-oauth2-oidc-jwks", "date-fns", "rxjs" ] diff --git a/lib/core/package.json b/lib/core/package.json index 75ee79a9c2..73ead1cc4d 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -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" }, diff --git a/lib/core/src/lib/auth/oidc/public-api.ts b/lib/core/src/lib/auth/oidc/public-api.ts index eab4c251c4..a8fa4a2f6b 100644 --- a/lib/core/src/lib/auth/oidc/public-api.ts +++ b/lib/core/src/lib/auth/oidc/public-api.ts @@ -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'; diff --git a/lib/core/src/lib/auth/oidc/redirect-auth.service.ts b/lib/core/src/lib/auth/oidc/redirect-auth.service.ts index fb06528ea4..2829c07cb0 100644 --- a/lib/core/src/lib/auth/oidc/redirect-auth.service.ts +++ b/lib/core/src/lib/auth/oidc/redirect-auth.service.ts @@ -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 { 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(() => { diff --git a/lib/core/src/lib/auth/oidc/web-crypto-jwks-validation-handler.spec.ts b/lib/core/src/lib/auth/oidc/web-crypto-jwks-validation-handler.spec.ts new file mode 100644 index 0000000000..d5a87447dc --- /dev/null +++ b/lib/core/src/lib/auth/oidc/web-crypto-jwks-validation-handler.spec.ts @@ -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 { + 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 { + 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 { + 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 { + 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: '); + }); + + 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(); + }); + }); +}); diff --git a/lib/core/src/lib/auth/oidc/web-crypto-jwks-validation-handler.ts b/lib/core/src/lib/auth/oidc/web-crypto-jwks-validation-handler.ts new file mode 100644 index 0000000000..cec7fb14c2 --- /dev/null +++ b/lib/core/src/lib/auth/oidc/web-crypto-jwks-validation-handler.ts @@ -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 { + 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 || '')); + } + + 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 { + const importAlgorithm = this.getImportAlgorithm(algorithmName); + return crypto.subtle.importKey('jwk', jsonWebKey, importAlgorithm, false, ['verify']); + } + + private async verifySignature(idToken: string, cryptoKey: CryptoKey, algorithmName: string): Promise { + 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 { + 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); + } + } +} diff --git a/package-lock.json b/package-lock.json index 8ee1cc65c3..4c05a38526 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6da646f697..0193db7369 100644 --- a/package.json +++ b/package.json @@ -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",