Private key JWT certificate verifier
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:
- The client generates an RSA or EC key pair.
- 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.
- 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. - 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. - 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 repo | https://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:
- 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 registeredtoken_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.
- Ensure the client is registered for
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);
}
});
}
}
}