node-oauth2-server
node-oauth2-server copied to clipboard
feat: add assertion framework support to client authentication
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
- Assertion Framework for OAuth 2.0 Client Authentication Authorization Grants
- JWT profile for OAuth 2.0 Client Authentication and Authorization Grants
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;
}