瀏覽代碼

Merge branch '1.4.x' into 1.5.x

Closes gh-2172
Joe Grandja 3 天之前
父節點
當前提交
6052cabd54

+ 5 - 1
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()));
 

+ 102 - 1
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().<String>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<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 +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<OidcClientRegistration, RegisteredClient> {
+
+		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();
+		}
+
+	}
+
 }