Private key JWT certificate verifier SPI

1. Qualified certificates for private key JWT signing

Clients that authenticate with the private_key_jwt method are normally required to have their public key for checking the JWT signature registered with the Connect2id server, by value in the jwks client metadata parameter, or by URL in the jwks_uri parameter.

Starting with Connect2id server 12.12 clients can also use an X.509 certificate to assert the signature verification key:

  1. The client generates an RSA or EC key pair.
  2. The client obtains a certificate from an authority (CA) recognised by the OpenID provider / OAuth 2.0 server. The certificate identifies the client in its subject field and the public key it holds comes from the pair the client had generated.
  3. For each private_key_jwt the client includes the certificate in the JWT x5c header parameter. The JWT is then signed as usual with the client's private key.
  4. Upon receiving a private_key_jwt authentication the Connect2id server invokes a deployment specific plugin (described here) to check if the client is required to use a certificate, and if so to verify it.
  5. If the certificate is good for the client the Connect2id server extracts its public key to check the JWT signature and complete the client authentication.

The Connect2id server provides a Service Provider Interface (SPI) that enables implementation of arbitrary certificate policies and verification process for a given authenticating client, based on the registered client metadata and other inputs.

Clients that will use private key JWT authentication with a certificate are not required to register a JWK set with the server. Note, to register a client for this authentication method without a JWK set, the Connect2id server must have loaded an enabled plugin for the certificate verification; if a plugin isn't present the server will reject the client registration request with an error message that a JWK set is needed.

The SPI is available since v12.12.

2. Private key JWT certificate verifier SPI

To plug in a certificate verifier for private_key_jwt implement the PrivateKeyJWTCertificateVerifier SPI defined in the Connect2id server toolkit:

Git repohttps://bitbucket.org/connect2id/server-sdk

Features of the SPI:

  • The Connect2id server will invoke the plugin for every private_key_jwt client authentication, regardless whether a certificate is supplied or not. Applies to all server endpoints where client authentication can take place.
  • The plugin determines whether an X.509 client certificate is required for the client or not, based on the registered client information and possibly other inputs.
  • The certificate can be supplied in two ways:
    • In the x5c header of the JWT assertion. This is the simpler and recommended method.
    • Registered with the Connect2id server in a public JWK using the standard x5c key parameter, where the JWK is included in a client JWK set registered by value (with jwks) or URL (jwks_uri).
  • The plugin is free to determine the actual certificate verification process, for instance whether revocation lists are checked.
  • If the client is required to include a certificate but failed to do so (in the expected location, see the example below), or the certificate is deemed invalid by the plugin, the Connect2id server will return an invalid_client error to the client.
  • The Connect2id server will perform the following security checks before invoking the plugin:
    • Ensure the client is registered for private_key_jwt authentication.
    • Ensure the JWT alg header matches the registered token_endpoint_auth_signing_alg for the client.
    • Ensure the x5c, if supplied, parses to a correctly formed X.509 certificate.
    • Ensure the public key type in the certificate satisfies the JWT algorithm.

If the Connect2id server detects an SPI implementation it will log its loading and enabled status under OP6221.

INFO MAIN [main] [OP6221] Loaded private_key_jwt certificate verifier: class=com.example.clientauth.MyCertificateVerifier enabled=true

3. Example

Sample plugin that looks up the client's certificate subject DN in the registered client metadata, using a custom cert_subject_dn field:

import java.security.cert.*;
import java.util.*;
import javax.security.auth.x500.*;
import net.minidev.json.*;
import com.nimbusds.oauth2.sdk.auth.verifier.*;
import com.nimbusds.openid.connect.provider.spi.clientauth.*;
import com.nimbusds.openid.connect.sdk.rp.*;

public class SampleVerifier implements PrivateKeyJWTCertificateVerifier {

    // Look up the certificate subject DN for the client in its registered
    // metadata
    private static Principal getExpectedCertificateSubject(
        final OIDCClientMetadata clientMetadata) {

        // Get the custom "data" field, of type JSON object
        var data = clientMetadata.getCustomField("data");
        if (! (data instanceof JSONObject)) {
            return null; // No custom client data found
        }
        var dataObject = (JSONObject) data;
        if (!dataObject.containsKey("cert_subject_dn")) {
            return null; // Client private_key_jwt without certificate
        }
        return new X500Principal((String) dataObject.get("cert_subject_dn"));
    }

    @Override
    public Optional<CertificateVerification> checkCertificateRequirement(
        final PrivateKeyJWTContext ctx) {

        Principal subject = getExpectedCertificateSubject(
            ctx.getOIDCClientInformation().getOIDCMetadata());

        if (expectedSubject == null) {
            return Optional.empty();
        } else {
            return Optional.of((x5c, certCtx) -> {

                if (! certCtx.getCertificateLocations().contains(
                    CertificateLocation.JWS_HEADER)) {
                    // The SPI works with certificates passed in the JWT x5c
                    // header as well as certificates registered in a client
                    // JWK x5c parameter. This plugin wants them in the JWT
                    // header.
                    throw new InvalidClientException("The certificate must be in the JWT x5c header");
                }

                // The x5c can be a full certificate chain, here we need the
                // client certificate only
                X509Certificate cert = x5c.get(0);

                if (! subject.equals(cert.getSubjectDN())) {
                    // The certificate was not issued to the OAuth client
                    throw new InvalidClientException("Bad certificate subject");
                }

                try {
                    // Verify the certificate using some PKIX logic. At a
                    // minimum this needs to ensure the certificate is signed
                    // by a recognised CA and the current time is within its
                    // validity window
                } catch (CertificateException | SignatureException e) {
                    throw new InvalidClientException("Invalid certificate: " + e.getMessage());
                } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException e) {
                    throw new ProviderException(e.getMessage(), e);
                }
            });
        }
    }
}