|
15 | 15 | */
|
16 | 16 | package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
17 | 17 |
|
| 18 | +import java.time.Duration; |
18 | 19 | import java.time.Instant;
|
19 | 20 | import java.time.temporal.ChronoUnit;
|
20 | 21 | import java.util.Collections;
|
|
31 | 32 | import jakarta.servlet.http.HttpServletResponse;
|
32 | 33 | import okhttp3.mockwebserver.MockResponse;
|
33 | 34 | import okhttp3.mockwebserver.MockWebServer;
|
| 35 | +import org.assertj.core.data.TemporalUnitWithinOffset; |
34 | 36 | import org.junit.jupiter.api.AfterAll;
|
35 | 37 | import org.junit.jupiter.api.AfterEach;
|
36 | 38 | import org.junit.jupiter.api.BeforeAll;
|
@@ -508,6 +510,46 @@ public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredCli
|
508 | 510 | assertThat(registeredClient.getClientSettings().<String>getSetting("non-registered-custom-metadata")).isNull();
|
509 | 511 | }
|
510 | 512 |
|
| 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 | + |
511 | 553 | private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
512 | 554 | // ***** (1) Obtain the "initial" access token used for registering the client
|
513 | 555 |
|
@@ -685,6 +727,48 @@ private Consumer<List<AuthenticationProvider>> configureClientRegistrationConver
|
685 | 727 |
|
686 | 728 | }
|
687 | 729 |
|
| 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 | + |
688 | 772 | @EnableWebSecurity
|
689 | 773 | @Configuration(proxyBeanMethods = false)
|
690 | 774 | static class AuthorizationServerConfiguration {
|
@@ -814,4 +898,25 @@ public OidcClientRegistration convert(RegisteredClient registeredClient) {
|
814 | 898 |
|
815 | 899 | }
|
816 | 900 |
|
| 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 | + } |
817 | 922 | }
|
0 commit comments