diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java index 84575aa8..7cd39a5c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,10 @@ public final class RegisteredClientOidcClientRegistrationConverter builder.clientSecret(registeredClient.getClientSecret()); } + if (registeredClient.getClientSecretExpiresAt() != null) { + builder.clientSecretExpiresAt(registeredClient.getClientSecretExpiresAt()); + } + builder.redirectUris((redirectUris) -> redirectUris.addAll(registeredClient.getRedirectUris())); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java index a2fc98fa..63239d02 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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; 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,40 @@ public class OidcClientRegistrationTests { assertThat(registeredClient.getClientSettings().getSetting("non-registered-custom-metadata")).isNull(); } + // gh-2111 + @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 +721,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> 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 +892,27 @@ 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 { + + private static final OidcClientRegistrationRegisteredClientConverter delegate = new OidcClientRegistrationRegisteredClientConverter(); + + @Override + public RegisteredClient convert(OidcClientRegistration clientRegistration) { + RegisteredClient registeredClient = delegate.convert(clientRegistration); + RegisteredClient.Builder registeredClientBuilder = RegisteredClient.from(registeredClient); + + Instant clientSecretExpiresAt = Instant.now().plus(Duration.ofHours(24)); + registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt); + + return registeredClientBuilder.build(); + } + + } + }