Skip to content

Commit a94096b

Browse files
whelephjgrandja
authored andcommitted
Fix to return client_secret_expires_at in client registration response
Issue gh-2111 Closes gh-2134 Signed-off-by: wheleph <[email protected]>
1 parent f1ab2bc commit a94096b

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public OidcClientRegistration convert(RegisteredClient registeredClient) {
4949
builder.clientSecret(registeredClient.getClientSecret());
5050
}
5151

52+
if (registeredClient.getClientSecretExpiresAt() != null) {
53+
builder.clientSecretExpiresAt(registeredClient.getClientSecretExpiresAt());
54+
}
55+
5256
builder.redirectUris((redirectUris) ->
5357
redirectUris.addAll(registeredClient.getRedirectUris()));
5458

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
1717

18+
import java.time.Duration;
1819
import java.time.Instant;
1920
import java.time.temporal.ChronoUnit;
2021
import java.util.Collections;
@@ -31,6 +32,7 @@
3132
import jakarta.servlet.http.HttpServletResponse;
3233
import okhttp3.mockwebserver.MockResponse;
3334
import okhttp3.mockwebserver.MockWebServer;
35+
import org.assertj.core.data.TemporalUnitWithinOffset;
3436
import org.junit.jupiter.api.AfterAll;
3537
import org.junit.jupiter.api.AfterEach;
3638
import org.junit.jupiter.api.BeforeAll;
@@ -508,6 +510,46 @@ public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredCli
508510
assertThat(registeredClient.getClientSettings().<String>getSetting("non-registered-custom-metadata")).isNull();
509511
}
510512

513+
/**
514+
* Scenario to validate that if there's a customization that sets client secret expiration date, then the date
515+
* is persisted and returned in the registration response
516+
*/
517+
@Test
518+
public void requestWhenClientRegistersWithSecretExpirationThenClientRegistrationResponse() throws Exception {
519+
this.spring.register(ClientSecretExpirationConfiguration.class).autowire();
520+
521+
// @formatter:off
522+
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
523+
.clientName("client-name")
524+
.redirectUri("https://client.example.com")
525+
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
526+
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
527+
.scope("scope1")
528+
.scope("scope2")
529+
.build();
530+
// @formatter:on
531+
532+
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
533+
534+
Instant expectedSecretExpiryDate = Instant.now().plus(Duration.ofHours(24));
535+
TemporalUnitWithinOffset allowedDelta = new TemporalUnitWithinOffset(1, ChronoUnit.MINUTES);
536+
537+
// Returned response contains expiration date
538+
assertThat(clientRegistrationResponse.getClientSecretExpiresAt())
539+
.isNotNull()
540+
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
541+
542+
RegisteredClient registeredClient = this.registeredClientRepository
543+
.findByClientId(clientRegistrationResponse.getClientId());
544+
545+
// Persisted RegisteredClient contains expiration date
546+
assertThat(registeredClient)
547+
.isNotNull();
548+
assertThat(registeredClient.getClientSecretExpiresAt())
549+
.isNotNull()
550+
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
551+
}
552+
511553
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
512554
// ***** (1) Obtain the "initial" access token used for registering the client
513555

@@ -685,6 +727,48 @@ private Consumer<List<AuthenticationProvider>> configureClientRegistrationConver
685727

686728
}
687729

730+
@EnableWebSecurity
731+
@Configuration(proxyBeanMethods = false)
732+
static class ClientSecretExpirationConfiguration extends AuthorizationServerConfiguration {
733+
734+
// @formatter:off
735+
@Bean
736+
@Override
737+
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
738+
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
739+
OAuth2AuthorizationServerConfigurer.authorizationServer();
740+
http
741+
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
742+
.with(authorizationServerConfigurer, (authorizationServer) ->
743+
authorizationServer
744+
.oidc((oidc) ->
745+
oidc
746+
.clientRegistrationEndpoint((clientRegistration) ->
747+
clientRegistration
748+
.authenticationProviders(configureClientRegistrationConverters())
749+
)
750+
)
751+
)
752+
.authorizeHttpRequests((authorize) ->
753+
authorize.anyRequest().authenticated()
754+
);
755+
return http.build();
756+
}
757+
// @formatter:on
758+
759+
private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
760+
// @formatter:off
761+
return (authenticationProviders) ->
762+
authenticationProviders.forEach((authenticationProvider) -> {
763+
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
764+
provider.setRegisteredClientConverter(new ClientSecretExpirationRegisteredClientConverter());
765+
}
766+
});
767+
// @formatter:on
768+
}
769+
770+
}
771+
688772
@EnableWebSecurity
689773
@Configuration(proxyBeanMethods = false)
690774
static class AuthorizationServerConfiguration {
@@ -814,4 +898,25 @@ public OidcClientRegistration convert(RegisteredClient registeredClient) {
814898

815899
}
816900

901+
/**
902+
* This customization adds client secret expiration time by setting {@code RegisteredClient.clientSecretExpiresAt}
903+
* during {@code OidcClientRegistration} -> {@code RegisteredClient} conversion
904+
*/
905+
private static final class ClientSecretExpirationRegisteredClientConverter
906+
implements Converter<OidcClientRegistration, RegisteredClient> {
907+
908+
private static final OidcClientRegistrationRegisteredClientConverter delegate =
909+
new OidcClientRegistrationRegisteredClientConverter();
910+
911+
@Override
912+
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
913+
RegisteredClient registeredClient = delegate.convert(clientRegistration);
914+
var registeredClientBuilder = RegisteredClient.from(registeredClient);
915+
916+
var clientSecretExpiresAt = Instant.now().plus(Duration.ofHours(24));
917+
registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt);
918+
919+
return registeredClientBuilder.build();
920+
}
921+
}
817922
}

0 commit comments

Comments
 (0)