@ -15,6 +15,7 @@
@@ -15,6 +15,7 @@
* /
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers ;
import java.time.Duration ;
import java.time.Instant ;
import java.time.temporal.ChronoUnit ;
import java.util.Collections ;
@ -31,6 +32,7 @@ import com.nimbusds.jose.proc.SecurityContext;
@@ -31,6 +32,7 @@ import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse ;
import okhttp3.mockwebserver.MockResponse ;
import okhttp3.mockwebserver.MockWebServer ;
import org.assertj.core.data.TemporalUnitWithinOffset ;
import org.junit.jupiter.api.AfterAll ;
import org.junit.jupiter.api.AfterEach ;
import org.junit.jupiter.api.BeforeAll ;
@ -508,6 +510,46 @@ public class OidcClientRegistrationTests {
@@ -508,6 +510,46 @@ public class OidcClientRegistrationTests {
assertThat ( registeredClient . getClientSettings ( ) . < String > getSetting ( "non-registered-custom-metadata" ) ) . isNull ( ) ;
}
/ * *
* Scenario to validate that if there ' s a customization that sets client secret expiration date , then the date
* is persisted and returned in the registration response
* /
@Test
public void requestWhenClientRegistersWithSecretExpirationThenClientRegistrationResponse ( ) throws Exception {
this . spring . register ( ClientSecretExpirationConfiguration . class ) . autowire ( ) ;
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration . builder ( )
. clientName ( "client-name" )
. redirectUri ( "https://client.example.com" )
. grantType ( AuthorizationGrantType . AUTHORIZATION_CODE . getValue ( ) )
. grantType ( AuthorizationGrantType . CLIENT_CREDENTIALS . getValue ( ) )
. scope ( "scope1" )
. scope ( "scope2" )
. build ( ) ;
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient ( clientRegistration ) ;
Instant expectedSecretExpiryDate = Instant . now ( ) . plus ( Duration . ofHours ( 24 ) ) ;
TemporalUnitWithinOffset allowedDelta = new TemporalUnitWithinOffset ( 1 , ChronoUnit . MINUTES ) ;
// Returned response contains expiration date
assertThat ( clientRegistrationResponse . getClientSecretExpiresAt ( ) )
. isNotNull ( )
. isCloseTo ( expectedSecretExpiryDate , allowedDelta ) ;
RegisteredClient registeredClient = this . registeredClientRepository
. findByClientId ( clientRegistrationResponse . getClientId ( ) ) ;
// Persisted RegisteredClient contains expiration date
assertThat ( registeredClient )
. isNotNull ( ) ;
assertThat ( registeredClient . getClientSecretExpiresAt ( ) )
. isNotNull ( )
. isCloseTo ( expectedSecretExpiryDate , allowedDelta ) ;
}
private OidcClientRegistration registerClient ( OidcClientRegistration clientRegistration ) throws Exception {
// ***** (1) Obtain the "initial" access token used for registering the client
@ -685,6 +727,48 @@ public class OidcClientRegistrationTests {
@@ -685,6 +727,48 @@ public class OidcClientRegistrationTests {
}
@EnableWebSecurity
@Configuration ( proxyBeanMethods = false )
static class ClientSecretExpirationConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain ( HttpSecurity http ) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer . authorizationServer ( ) ;
http
. securityMatcher ( authorizationServerConfigurer . getEndpointsMatcher ( ) )
. with ( authorizationServerConfigurer , ( authorizationServer ) - >
authorizationServer
. oidc ( ( oidc ) - >
oidc
. clientRegistrationEndpoint ( ( clientRegistration ) - >
clientRegistration
. authenticationProviders ( configureClientRegistrationConverters ( ) )
)
)
)
. authorizeHttpRequests ( ( authorize ) - >
authorize . anyRequest ( ) . authenticated ( )
) ;
return http . build ( ) ;
}
// @formatter:on
private Consumer < List < AuthenticationProvider > > configureClientRegistrationConverters ( ) {
// @formatter:off
return ( authenticationProviders ) - >
authenticationProviders . forEach ( ( authenticationProvider ) - > {
if ( authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider ) {
provider . setRegisteredClientConverter ( new ClientSecretExpirationRegisteredClientConverter ( ) ) ;
}
} ) ;
// @formatter:on
}
}
@EnableWebSecurity
@Configuration ( proxyBeanMethods = false )
static class AuthorizationServerConfiguration {
@ -814,4 +898,25 @@ public class OidcClientRegistrationTests {
@@ -814,4 +898,25 @@ public class OidcClientRegistrationTests {
}
/ * *
* This customization adds client secret expiration time by setting { @code RegisteredClient . clientSecretExpiresAt }
* during { @code OidcClientRegistration } - > { @code RegisteredClient } conversion
* /
private static final class ClientSecretExpirationRegisteredClientConverter
implements Converter < OidcClientRegistration , RegisteredClient > {
private static final OidcClientRegistrationRegisteredClientConverter delegate =
new OidcClientRegistrationRegisteredClientConverter ( ) ;
@Override
public RegisteredClient convert ( OidcClientRegistration clientRegistration ) {
RegisteredClient registeredClient = delegate . convert ( clientRegistration ) ;
var registeredClientBuilder = RegisteredClient . from ( registeredClient ) ;
var clientSecretExpiresAt = Instant . now ( ) . plus ( Duration . ofHours ( 24 ) ) ;
registeredClientBuilder . clientSecretExpiresAt ( clientSecretExpiresAt ) ;
return registeredClientBuilder . build ( ) ;
}
}
}