Skip to content

Conversation

dhensby
Copy link
Contributor

@dhensby dhensby commented Mar 14, 2025

Summary

I'm looking to implement client assertion support. At the moment I'm leaning to keeping as much of this in
user-land code, but maybe we should have a way to register client authentication plugins (in a similar way that
we do with grant types). I think that might take quite a refactor (and breaking changes) to allow for that.

At the moment this is just a draft to check what the minimum interface is to get this working in user-land.


Some way to inject a body parser/interpreter is needed because the assertions contain most of the relevant request data that is typically just a property in the request body and most of the library makes an assumption that all the data is props in the body.

Linked issue(s)

N/A

Involved parts of the project

Client authentication

Added tests?

todo

OAuth2 standard

Example

This would be implemented something like this to support client assertions with JWT:

getting a token:

 const token = await server.token(request, response, { requestProcessor: (incoming: OAuth2Server.Request) => { // determine if this is a request we can process (ie: a client assertion) if (isClientAssertionRequest(incoming)) { // just read out the JWT data - this isn't being verified as genuine at this point, that comes later - we just want to know who this request is *claiming* to be. const { scope, sub: client_id } = decodeJwt<{ scope?: string }>(incoming.body.client_assertion); return { client_id, client_assertion: incoming.body.client_assertion, client_assertion_type: incoming.body.client_assertion_type, grant_type: incoming.body.grant_type, code_verifier: incoming.body.code_verifier, scope, }; } return incoming.body as TokenRequest; }, });

Implementing getClientFromAssertion:

 async getClientFromAssertion(assertion: OAuth2Server.AssertionCredential): Promise<OAuth2Server.Client | OAuth2Server.Falsey> { // verify we can handle this assertion type if (assertion.clientAssertionType !== 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') { throw new InvalidClientError('client_assertion_type not supported'); } // first thing is to try to extract the client id from the assertion // if a clientId is present, check it matches (as per spec - it's optional but must match if supplied) // then find their key and validate the assertion const { sub: clientId } = decodeJwt(assertion.clientAssertion); if (!clientId) { throw new InvalidClientError('client_assertion malformed'); } if (assertion.clientId && assertion.clientId !== clientId) { throw new InvalidClientError('client_id mismatch'); } // somehow get the client and their keys const client = await getClient(clientId); const jwks = createLocalJWKSet({ keys: client.keys }); // actually verify the assertion is genuine/trusted await jwtVerify(assertion.clientAssertion, jwks, { requiredClaims: [ 'iss', 'sub', 'aud', 'exp', ], maxTokenAge: 60, audience: ['https://example.com'], }).catch((e) => { return Promise.reject(new InvalidClientError(e as Error)); }); // any other validation can be performed here (eg: issuer is acceptable, audience, etc) return client; }
@dhensby dhensby force-pushed the pulls/assertion-framework-support branch 2 times, most recently from 5e54253 to 80571a7 Compare April 11, 2025 09:23
@dhensby dhensby force-pushed the pulls/assertion-framework-support branch from 80571a7 to 95cbfc5 Compare April 11, 2025 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants