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
5 changes: 5 additions & 0 deletions .changeset/thin-pears-attack.md
Original file line number Diff line number Diff line change
@@ -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`.
33 changes: 4 additions & 29 deletions packages/core/src/monocloud-oidc-backend-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -515,29 +504,15 @@ 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(
"Access token does not contain a 'cnf' (confirmation) claim for certificate binding"
);
}

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");
}

Expand Down
38 changes: 6 additions & 32 deletions packages/core/tests/introspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-----`;
Expand All @@ -1051,7 +1051,7 @@ AQIDBAUGBwg=
validateCertificateBinding: 'required',
clientCertificate: certificate,
}),
"Malformed 'cnf' claim for certificate binding"
"The 'cnf' claim could not be parsed"
);

fetchSpy.assert();
Expand Down Expand Up @@ -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()
Expand All @@ -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();
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);

Expand All @@ -1471,7 +1445,7 @@ AQIDBAUGBwg=
.configureIntrospection({
responseBody: {
...baseClaims,
cnf: JSON.stringify({ 'x5t#S256': certificateHash }),
cnf: { 'x5t#S256': certificateHash },
},
})
.createSpy();
Expand Down
Loading