Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/javascript/src/DefaultCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class DefaultCrypto implements Crypto<Uint8Array> {
clockTolerance?: number,
validateJwtIssuer = true,
): Promise<boolean> {
const key: jose.KeyLike | Uint8Array = await jose.importJWK(jwk as jose.JWK);
const key: jose.CryptoKey | Uint8Array = await jose.importJWK(jwk as jose.JWK);

await jose.jwtVerify(idToken, key, {
algorithms,
Expand Down
90 changes: 90 additions & 0 deletions packages/javascript/src/__tests__/DefaultCrypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you 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 {exportJWK, generateKeyPair, SignJWT} from 'jose';
import {DefaultCrypto} from '../DefaultCrypto';
import TokenConstants from '../constants/TokenConstants';
import {JWKInterface} from '../models/crypto';

describe('DefaultCrypto.verifyJwt with ML-DSA (AKP JWKs)', (): void => {
it.each(['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'] as const)(
'verifies an id_token signed with %s against its AKP JWK',
async (alg): Promise<void> => {
const {publicKey, privateKey} = await generateKeyPair(alg, {extractable: true});
const jwk = (await exportJWK(publicKey)) as unknown as JWKInterface;
jwk.alg = alg;
jwk.kid = `test-${alg}`;
jwk.use = 'sig';

const idToken: string = await new SignJWT({})
.setProtectedHeader({alg, kid: jwk.kid})
.setIssuedAt()
.setIssuer('https://issuer.example')
.setAudience('client-id')
.setSubject('user-id')
.setExpirationTime('1h')
.sign(privateKey);

const crypto = new DefaultCrypto();
const result: boolean = await crypto.verifyJwt(
idToken,
jwk,
TokenConstants.SignatureValidation.SUPPORTED_ALGORITHMS as unknown as string[],
'client-id',
'https://issuer.example',
'user-id',
);

expect(result).toBe(true);
},
);

it('rejects an ML-DSA id_token with a tampered signature', async (): Promise<void> => {
const alg = 'ML-DSA-65';
const {publicKey, privateKey} = await generateKeyPair(alg, {extractable: true});
const jwk = (await exportJWK(publicKey)) as unknown as JWKInterface;
jwk.alg = alg;
jwk.kid = 'test-tampered';
jwk.use = 'sig';

const idToken: string = await new SignJWT({})
.setProtectedHeader({alg, kid: jwk.kid})
.setIssuedAt()
.setIssuer('https://issuer.example')
.setAudience('client-id')
.setSubject('user-id')
.setExpirationTime('1h')
.sign(privateKey);

const parts: string[] = idToken.split('.');
const tamperedSignature: string = parts[2].slice(0, -4) + (parts[2].endsWith('AAAA') ? 'BBBB' : 'AAAA');
const tamperedToken = `${parts[0]}.${parts[1]}.${tamperedSignature}`;

const crypto = new DefaultCrypto();
await expect(
crypto.verifyJwt(
tamperedToken,
jwk,
TokenConstants.SignatureValidation.SUPPORTED_ALGORITHMS as unknown as string[],
'client-id',
'https://issuer.example',
'user-id',
),
).rejects.toThrow();
});
});
3 changes: 2 additions & 1 deletion packages/javascript/src/constants/TokenConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ const TokenConstants: {
* - `RS512` - RSASSA-PKCS1-v1_5 using SHA-512
* - `RS384` - RSASSA-PKCS1-v1_5 using SHA-384
* - `PS256` - RSASSA-PSS using SHA-256 and MGF1 with SHA-256
* - `ML-DSA-44` / `ML-DSA-65` / `ML-DSA-87` - post-quantum ML-DSA (RFC 9864), AKP JWKs
*/
SUPPORTED_ALGORITHMS: ['RS256', 'RS512', 'RS384', 'PS256'],
SUPPORTED_ALGORITHMS: ['RS256', 'RS512', 'RS384', 'PS256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'],
},

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/javascript/src/models/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
*/
export interface JWKInterface {
alg: string;
e: string;
e?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change intentional? why have we marked e and n as optional attributes?

@hwupathum hwupathum Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the JWK format of MLDSA algorithm is different and does not have these parameters

kid: string;
kty: string;
n: string;
n?: string;
/** Raw public key bytes (base64url), for AKP JWKs (RFC 9864, e.g. ML-DSA). */
pub?: string;
use: string;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/utils/NodeCryptoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class NodeCryptoUtils implements Crypto<Buffer | string> {
subject: string,
clockTolerance?: number,
): Promise<boolean> {
const key: jose.KeyLike | Uint8Array = await jose.importJWK(jwk);
const key: jose.CryptoKey | Uint8Array = await jose.importJWK(jwk);
return jose
.jwtVerify(idToken, key, {
algorithms,
Expand Down
20 changes: 10 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ catalog:
dompurify: 3.4.11
eslint: 9.39.4
fast-sha256: 1.3.0
jose: 5.2.0
jose: 6.2.3
jsdom: 27.0.1
memory-cache: 0.2.0
playwright: 1.60.0
Expand Down
Loading