-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
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.