Skip to content

client_secret_expires_at is not populated in /connect/register response #2111

@wheleph

Description

@wheleph

Describe the bug

According to the Client Registration Response Specification it must contain the field client_secret_expires_at if client_secret is issued.

Spring Authorization Server indeed sets the field to 0 by default since the secret does not expire.

However if I customize SAS by setting an expiry date, then 0 is still returned.

Steps To Reproduce

Define a custom converter (OidcClientRegistration -> RegisteredClient) that sets client secret expiration date in the constructed RegisteredClient:

@Component
public final class EnhancedOidcClientRegistrationRegisteredClientConverter
    implements Converter<OidcClientRegistration, RegisteredClient> {

    private static final OidcClientRegistrationRegisteredClientConverter DEFAULT_CLIENT_REGISTRATION_CONVERTER =
        new OidcClientRegistrationRegisteredClientConverter();

    @Override
    public RegisteredClient convert(OidcClientRegistration clientRegistration) {
        RegisteredClient registeredClient = DEFAULT_CLIENT_REGISTRATION_CONVERTER.convert(clientRegistration);
        var registeredClientBuilder = RegisteredClient.from(registeredClient);

        var clientSecretExpiresAt = Instant.now().plus(Duration.ofDays(30));
        registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt);

        return registeredClientBuilder.build();
    }
}

Use the converter in SAS security configuration:

        var authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer()
            .oidc(oidcConfigurer -> oidcConfigurer.clientRegistrationEndpoint(
                clientRegistrationEndpointConfigurer -> clientRegistrationEndpointConfigurer
                    .authenticationProviders(providers -> providers.forEach(provider -> {
                        if (provider instanceof OidcClientRegistrationAuthenticationProvider clientRegistrationProvider) {
                            clientRegistrationProvider.setRegisteredClientConverter(registeredClientConverter);
                        }
                    }))
            ));

Dynamically register a new client via:

curl -X POST --location "http://localhost:8080/connect/register" \
    -H "Authorization: Bearer <redacted>" \
    -H "Content-Type: application/json" \
    -d '{
          "redirect_uris": ["https://example.com"],
          "client_name": "test_dynamic_client",
          "grant_types": ["client_credentials"],
          "token_endpoint_auth_method": "client_secret_basic",
          "scope": "foo,bar"
        }'

Observe "client_secret_expires_at": 0 in the response:

{
  "client_id": "KMr9EnA3KOVlJHbjm4LnF_O6wJRjWj4L728og1s4ic8",
  "client_id_issued_at": 1753029697,
  "client_name": "test_dynamic_client",
  "client_secret": "3ubvxYLg0lIjAuE1eWrNBNdAorApCeoVndZLpke8Ni3NPV9CFiXd-gEYynd2gp8o",
  "redirect_uris": [
    "https://example.com"
  ],
  "grant_types": [
    "client_credentials",
    "authorization_code"
  ],
  "response_types": [
    "code"
  ],
  "scope": "foo,bar",
  "token_endpoint_auth_method": "client_secret_basic",
  "id_token_signed_response_alg": "RS256",
  "registration_client_uri": "http://localhost:8080/connect/register?client_id=KMr9EnA3KOVlJHbjm4LnF_O6wJRjWj4L728og1s4ic8",
  "registration_access_token": "eyJraWQiOiI4YmRiMjcwMS1hMzk1LTQ3N2UtYTA3YS1mNzlkZTA5NWNkOWUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJLTXI5RW5BM0tPVmxKSGJqbTRMbkZfTzZ3SlJqV2o0TDcyOG9nMXM0aWM4IiwiYXVkIjoiS01yOUVuQTNLT1ZsSkhiam00TG5GX082d0pSaldqNEw3MjhvZzFzNGljOCIsIm5iZiI6MTc1MzAyOTY5Nywic2NvcGUiOlsiY2xpZW50LnJlYWQiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNzUzMDI5OTk3LCJpYXQiOjE3NTMwMjk2OTcsImp0aSI6IjAzOGJmZmE1LWIxNjYtNDY1Mi1iM2NhLTM5MDEzMTc0OGFjMCJ9.TkMeF93HMALBJj9PTTRW-zSGC-LTp7S-H2EmU2U9oqzF3iVuph3sBrm5Sm8Uj8dqocKTWV48ZWLMbZVD9jhrJHO0gAv7wyJgCEMqZpnZSJGDY8kMcVbqZsQUHJn4W72gKTYTM_oGFUnjdBb-K3l1RZHmir-tmaTIaFNllyp-8K5Kx-dZSrmqtCYjXTIV8l4EUS5FV9dFDqqHN22R9eDtan-9EmphKINrfDVgMZkQKzCthdR_A7uP_B-8f9eU5XOGyLgjI_JcdPeaH2qQGmKnCi7qqLyxXhcJu1IUx9yoUYI4eZnR7R2Jbe2DUqyfGNffO45Gj1ej6VYS4ijh1gRSMQ",
  "client_secret_expires_at": 0
}

Expected behavior

client_secret_expires_at should be set to some non-zero vaule:

{
  "client_id": "KUWFMRDQRqa1-8J4-JXeJLXxbukDA5QDhiKRLls17yA",
  "client_id_issued_at": 1753029789,
  "client_name": "test_dynamic_client",
  "client_secret": "2RA_-RTaAwgvFdLb3iWqgx2QfFrwty1yGKv9ZNgjLQ0w2W0SDj8MeidHVBjfzmHf",
  "redirect_uris": [
    "https://example.com"
  ],
  "grant_types": [
    "client_credentials",
    "authorization_code"
  ],
  "response_types": [
    "code"
  ],
  "scope": "foo,bar",
  "token_endpoint_auth_method": "client_secret_basic",
  "id_token_signed_response_alg": "RS256",
  "registration_client_uri": "http://localhost:8080/connect/register?client_id=KUWFMRDQRqa1-8J4-JXeJLXxbukDA5QDhiKRLls17yA",
  "client_secret_expires_at": 1755621789,
  "registration_access_token": "eyJraWQiOiJkMmE5YWE4Yy00NWIxLTQ4YjQtYTRjNS1lYTcyMjhhZTYzNTIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJLVVdGTVJEUVJxYTEtOEo0LUpYZUpMWHhidWtEQTVRRGhpS1JMbHMxN3lBIiwiYXVkIjoiS1VXRk1SRFFScWExLThKNC1KWGVKTFh4YnVrREE1UURoaUtSTGxzMTd5QSIsIm5iZiI6MTc1MzAyOTc4OSwic2NvcGUiOlsiY2xpZW50LnJlYWQiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNzUzMDMwMDg5LCJpYXQiOjE3NTMwMjk3ODksImp0aSI6IjA2NjY5NzQ4LWM2MjktNDUyNS05NzhhLTA3M2E4YTE2YWE5ZCJ9.f-NkkkmiEkaap8WX_yqxja8F0EhyG8PivV53p36ak6KQTK8s6Dto6pT5wQk8bTeETaOXe0ijxfiU30v6Nm3m_IKD3AsW0RxcraXjprj4TDz_B7xxsvGn4ixBakQhhjtY9ZEg3QYJAYd4rKmIdFGBQIX4Axd5JO4JsjjVu7dIpq0c-zICvB9uh7YdIPAgnR9LOJBSopAeNu0lZdsV3E5NU6lOTwlg0J6X42NxcldttcNG6GuaBHScb0GVBXi0vS082fSxzbE284v5DSOUAVlJ-CgDJ59iXJ-L6td_MawdeaKhSkLMySBGwbRLXCK7VwTX1NTqeDG1sFsHDqQ1zE8m4w"
}

Sample

https://github.com/wheleph/sas-client-secret-expiration

Possible fix

The standard RegisteredClientOidcClientRegistrationConverter performs opposite conversion (RegisteredClient -> OidcClientRegistration) and it does copy client_secret value. However it completely ignores client_secret_expires_at:

		if (registeredClient.getClientSecret() != null) {
			builder.clientSecret(registeredClient.getClientSecret());
		}

The class could be enhanced to copy client_secret_expires_at as well if it's present.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions