From e5260689ad7c3f1c24ba0cf3f45ba35f01c4ab6e Mon Sep 17 00:00:00 2001 From: lakshayysaini Date: Wed, 23 Sep 2026 15:01:12 +0530 Subject: [PATCH] fix(core): remove cnf claim string check --- .changeset/thin-pears-attack.md | 5 +++ .../core/src/monocloud-oidc-backend-client.ts | 33 ++-------------- packages/core/tests/introspect.test.ts | 38 +++---------------- 3 files changed, 15 insertions(+), 61 deletions(-) create mode 100644 .changeset/thin-pears-attack.md diff --git a/.changeset/thin-pears-attack.md b/.changeset/thin-pears-attack.md new file mode 100644 index 00000000..f3f728da --- /dev/null +++ b/.changeset/thin-pears-attack.md @@ -0,0 +1,5 @@ +--- +'@monocloud/auth-core': patch +--- + +Require the `cnf` confirmation claim to be a JSON object, per RFC 7800. A `cnf` carried as a JSON-encoded string is no longer parsed and is rejected with `The 'cnf' claim could not be parsed`. diff --git a/packages/core/src/monocloud-oidc-backend-client.ts b/packages/core/src/monocloud-oidc-backend-client.ts index 7f634b72..f485bf0b 100644 --- a/packages/core/src/monocloud-oidc-backend-client.ts +++ b/packages/core/src/monocloud-oidc-backend-client.ts @@ -37,22 +37,11 @@ const isCertificateBoundCnf = (cnf: unknown): boolean => { return false; } - let value: unknown = cnf; - - if (typeof value === 'string') { - try { - value = JSON.parse(value) as unknown; - } catch { - return true; - } - } - - // A `cnf` that cannot be parsed is treated as certificate-bound, so that validation runs and rejects it rather than silently skipping a broken claim. - if (value === null || typeof value !== 'object' || Array.isArray(value)) { + if (typeof cnf !== 'object' || Array.isArray(cnf)) { return true; } - return 'x5t#S256' in value; + return 'x5t#S256' in cnf; }; /** @@ -515,7 +504,7 @@ export class MonoCloudOidcBackendClient extends MonoCloudOidcClientBase { new Uint8Array(certificateDigest) ); - let cnfClaimValue: unknown = accessTokenClaims.cnf; + const cnfClaimValue: unknown = accessTokenClaims.cnf; if (cnfClaimValue === undefined || cnfClaimValue === null) { throw new MonoCloudTokenError( @@ -523,21 +512,7 @@ export class MonoCloudOidcBackendClient extends MonoCloudOidcClientBase { ); } - if (typeof cnfClaimValue === 'string') { - try { - cnfClaimValue = JSON.parse(cnfClaimValue) as unknown; - } catch { - throw new MonoCloudTokenError( - "Malformed 'cnf' claim for certificate binding" - ); - } - } - - if ( - cnfClaimValue === null || - typeof cnfClaimValue !== 'object' || - Array.isArray(cnfClaimValue) - ) { + if (typeof cnfClaimValue !== 'object' || Array.isArray(cnfClaimValue)) { throw new MonoCloudTokenError("The 'cnf' claim could not be parsed"); } diff --git a/packages/core/tests/introspect.test.ts b/packages/core/tests/introspect.test.ts index e691b225..951770dd 100644 --- a/packages/core/tests/introspect.test.ts +++ b/packages/core/tests/introspect.test.ts @@ -1025,7 +1025,7 @@ AQIDBAUGBwg= fetchSpy.assert(); }); - it("should throw if the 'cnf' claim is malformed JSON", async () => { + it("should throw if the 'cnf' claim is a string", async () => { const certificate = `-----BEGIN CERTIFICATE----- AQIDBAUGBwg= -----END CERTIFICATE-----`; @@ -1051,7 +1051,7 @@ AQIDBAUGBwg= validateCertificateBinding: 'required', clientCertificate: certificate, }), - "Malformed 'cnf' claim for certificate binding" + "The 'cnf' claim could not be parsed" ); fetchSpy.assert(); @@ -1246,7 +1246,7 @@ AQIDBAUGBwg= fetchSpy.assert(); }); - it("should throw under 'when_present' when the cnf claim is malformed", async () => { + it("should throw under 'when_present' when the cnf claim is a string", async () => { const certificate = 'AQIDBAUGBwg='; const fetchSpy = fetchBuilder() @@ -1270,7 +1270,7 @@ AQIDBAUGBwg= validateCertificateBinding: 'when_present', clientCertificate: certificate, }), - "Malformed 'cnf' claim for certificate binding" + "The 'cnf' claim could not be parsed" ); fetchSpy.assert(); @@ -1302,32 +1302,6 @@ AQIDBAUGBwg= fetchSpy.assert(); }); - it("should skip binding under 'when_present' when a string cnf claim uses a different confirmation method", async () => { - const fetchSpy = fetchBuilder() - .configureMetadata() - .configureIntrospection({ - responseBody: { - ...baseClaims, - cnf: JSON.stringify({ jkt: 'dpop-thumbprint' }), - }, - }) - .createSpy(); - - const client = new MonoCloudOidcBackendClient( - 'example.com', - 'https://api.example.com', - defaultClientOptions - ); - - const result = await client.introspectAccessToken('some-token', { - validateCertificateBinding: 'when_present', - }); - - expect(result.sub).toBe('user123'); - - fetchSpy.assert(); - }); - it("should throw under 'when_present' when the cnf claim is not an object", async () => { const fetchSpy = fetchBuilder() .configureMetadata() @@ -1462,7 +1436,7 @@ AQIDBAUGBwg= fetchSpy.assert(); }); - it('should validate certificate binding with base64 certificate and string cnf claim', async () => { + it('should validate certificate binding with a bare base64 certificate', async () => { const certificate = 'AQIDBAUGBwg='; const certificateHash = await getCertificateHash(certificate); @@ -1471,7 +1445,7 @@ AQIDBAUGBwg= .configureIntrospection({ responseBody: { ...baseClaims, - cnf: JSON.stringify({ 'x5t#S256': certificateHash }), + cnf: { 'x5t#S256': certificateHash }, }, }) .createSpy();