node-oauth2-server icon indicating copy to clipboard operation
node-oauth2-server copied to clipboard

feat: add assertion framework support to client authentication

Open dhensby opened this issue 8 months ago • 0 comments

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 avatar Mar 14 '25 11:03 dhensby