Client secret store codec SPI

1. Encrypt or hash client secrets in storage

Connect2id server deployments with a backend database which doesn't encrypt its data can implement server-side encryption of the client secrets. This is enabled by a dedicated plugin interface (SPI) for applying transformations to the client_secret before writing it to the database, and then again upon its retrieval.

Encrypting, or alternatively, hashing the client secrets, is a recommended security measure to minimise the risk of an undetected credential leak from the database.

Recommended methods:

Note, while hashing is easier to implement because there is no symmetric key to be managed, it cannot be used with a client using client_secret_jwt authentication or an HMAC JOSE algorithm that requires the Connect2id server to have the secret in clear text. This means clients registered for the following must use encryption instead of hashing of the client_secret:

  • Clients registered for client_secret_jwt authentication.
  • Clients registered or ID token, UserInfo, request object (JAR) or JARM with the JWS algorithms HS256, HS384 or HS512.
  • Clients registered to receive or submit JWTs encrypted with a shared AES key derived from the secret, the JWE algorithm dir.

Connect2id server deployment can also choose to dispense with the client secrets entirely, which is a wise security strategy, by allowing only public key based authentication - private_key_jwt, or the tls_client_auth / self_signed_tls_client_auth methods in RFC 8705.

The codec plugin SPI can also be used to import client secrets of arbitrary format from another server.

2. Client secret codec SPI

To plug in your own custom codec implement the ClientSecretStoreCodec SPI defined in the Connect2id server toolkit:

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

Features of the client secret codec SPI:

  • Supports arbitrary encoding of client_secret values before writing them to the database.
  • Supports arbitrary decoding of client_secret values after retrieving them from the database.
  • Supports separate encoding / decoding of client_secret values for clients imported from another OAuth 2.0 server.
  • Provides access to the registered information for the client so that encoding decisions (e.g. encrypt or hash) can be made.
  • Provides access to the Connect2id server JWK set where the client secret encryption AES key(s) can be stored.

If the Connect2id server detects an SPI implementation it will log its loading under OP0138.

INFO main MAIN - [OP0138] Loaded client secret store codec: com.nimbusds.openid.connect.provider.spi.secrets.impl.BCryptSecretCodec

3. Example codec

Sample BCrypt hashing codec with the BCrypt Java library:

import at.favre.lib.crypto.bcrypt.BCrypt;
import com.nimbusds.openid.connect.provider.spi.secrets.*;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;

public class BCryptCodec implements ClientSecretStoreCodec {

    @Override
    public String encode(final Secret secret, final SecretCodecContext ctx) {
        return BCrypt
            .withDefaults()
            .hashToString(4, secret.getValue().toCharArray());
    }

    @Override
    public DecodedSecret decode(final String storedValue, final SecretCodecContext ctx) {

        return DecodedSecret.createForHashedSecret(
            otherSecret -> {
                BCrypt.Result result = BCrypt
                    .verifyer()
                    .verify(
                        otherSecret.getValue().toCharArray(),
                        storedValue.toCharArray());
                return result.verified;
            }
        );
    }
}

Example transformation with BCrypt:

Plain text 256-bit secret, BASE64URL encoded: 5k4NOArtKpDYeBoxDoVwXswsIApyibpMIBWRgLdSyNM
BCrypt: $2a$04$a9uQ9Ka0usxqTCp/1je2iuS.qnVsXKe0Gjhh5kPEhnbInkseODhgS

4. Effect on the client registration API

If the codec cannot reverse the client secret because it is hashed the client information responses will contain a client_secret, but its value will be set to null.

Example response indicating a hashed client secret:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "client_id"                : "s6BhdRkqt3",
  "client_secret"            : null,
  "client_secret_expires_at" : 1577858400
}

5. How to return the encoded secret in the client registration API

To return the encoded (stored) client secret in client information responses the codec plugin must call the withEncodedValue method before returning the DecodedSecret.

@Override
public DecodedSecret decode(final String storedValue, final SecretCodecContext ctx) {

    return DecodedSecret.createForHashedSecret(
            otherSecret -> {
                BCrypt.Result result = BCrypt
                    .verifyer()
                    .verify(
                        otherSecret.getValue().toCharArray(),
                        storedValue.toCharArray());
                return result.verified;
            }
        ).withEncodedValue(storedValue);
}

The encoded value will then appear in a custom client_secret_encoded field.

Example response including the hashed client secret value:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "client_id"                : "s6BhdRkqt3",
  "client_secret"            : null,
  "client_secret_encoded"    : "$2a$04$a9uQ9Ka0usxqTCp/1je2iuS.qnVsXKe0Gjhh5kPEhnbInkseODhgS",
  "client_secret_expires_at" : 1577858400
}

Note, the client_secret_encoded will appear only in client read responses.

6. How to import hashed client secrets

The custom preferred_client_secret field in a client registration request can be used to import hashed or otherwise encoded secrets from another OAuth 2.0 / OpenID Connect server. The value of the preferred_client_secret will appear as input to the encodedImported method of the ClientSecretCodec.