|
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 | + var expectedSecretExpiryDate = Instant.now().plus(Duration.ofHours(24)); |
| 535 | + var 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 |
|
@@ -643,8 +685,40 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
643 | 685 |
|
644 | 686 | @EnableWebSecurity
|
645 | 687 | @Configuration(proxyBeanMethods = false)
|
646 |
| - static class CustomClientMetadataConfiguration extends AuthorizationServerConfiguration { |
| 688 | + static class CustomClientMetadataConfiguration extends ClientRegistrationConvertersConfiguration { |
| 689 | + |
| 690 | + private static final List<String> supportedCustomClientMetadata = List.of("custom-metadata-name-1", "custom-metadata-name-2"); |
| 691 | + |
| 692 | + @Override |
| 693 | + protected Converter<OidcClientRegistration, RegisteredClient> registeredClientConverter() { |
| 694 | + return new CustomRegisteredClientConverter(supportedCustomClientMetadata); |
| 695 | + } |
| 696 | + |
| 697 | + @Override |
| 698 | + protected Converter<RegisteredClient, OidcClientRegistration> oidcClientRegistrationConverter() { |
| 699 | + return new CustomClientRegistrationConverter(supportedCustomClientMetadata); |
| 700 | + } |
| 701 | + |
| 702 | + } |
| 703 | + |
| 704 | + @EnableWebSecurity |
| 705 | + @Configuration(proxyBeanMethods = false) |
| 706 | + static class ClientSecretExpirationConfiguration extends ClientRegistrationConvertersConfiguration { |
| 707 | + |
| 708 | + @Override |
| 709 | + protected Converter<OidcClientRegistration, RegisteredClient> registeredClientConverter() { |
| 710 | + return new ClientSecretExpirationRegisteredClientConverter(); |
| 711 | + } |
| 712 | + |
| 713 | + } |
647 | 714 |
|
| 715 | + /** |
| 716 | + * This test configuration allows to override {@code RegisteredClient} -> {@code OidcClientRegistration} and |
| 717 | + * {@code OidcClientRegistration} -> {@code RegisteredClient} converters |
| 718 | + */ |
| 719 | + @EnableWebSecurity |
| 720 | + @Configuration(proxyBeanMethods = false) |
| 721 | + static class ClientRegistrationConvertersConfiguration extends AuthorizationServerConfiguration { |
648 | 722 | // @formatter:off
|
649 | 723 | @Bean
|
650 | 724 | @Override
|
@@ -674,15 +748,27 @@ private Consumer<List<AuthenticationProvider>> configureClientRegistrationConver
|
674 | 748 | // @formatter:off
|
675 | 749 | return (authenticationProviders) ->
|
676 | 750 | authenticationProviders.forEach((authenticationProvider) -> {
|
677 |
| - List<String> supportedCustomClientMetadata = List.of("custom-metadata-name-1", "custom-metadata-name-2"); |
678 | 751 | if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
|
679 |
| - provider.setRegisteredClientConverter(new CustomRegisteredClientConverter(supportedCustomClientMetadata)); |
680 |
| - provider.setClientRegistrationConverter(new CustomClientRegistrationConverter(supportedCustomClientMetadata)); |
| 752 | + var registeredClientConverter = registeredClientConverter(); |
| 753 | + if (registeredClientConverter != null) { |
| 754 | + provider.setRegisteredClientConverter(registeredClientConverter); |
| 755 | + } |
| 756 | + var oidcClientRegistrationConverter = oidcClientRegistrationConverter(); |
| 757 | + if (oidcClientRegistrationConverter != null) { |
| 758 | + provider.setClientRegistrationConverter(oidcClientRegistrationConverter); |
| 759 | + } |
681 | 760 | }
|
682 | 761 | });
|
683 | 762 | // @formatter:on
|
684 | 763 | }
|
685 | 764 |
|
| 765 | + protected Converter<OidcClientRegistration, RegisteredClient> registeredClientConverter() { |
| 766 | + return null; |
| 767 | + } |
| 768 | + |
| 769 | + protected Converter<RegisteredClient, OidcClientRegistration> oidcClientRegistrationConverter() { |
| 770 | + return null; |
| 771 | + } |
686 | 772 | }
|
687 | 773 |
|
688 | 774 | @EnableWebSecurity
|
@@ -814,4 +900,26 @@ public OidcClientRegistration convert(RegisteredClient registeredClient) {
|
814 | 900 |
|
815 | 901 | }
|
816 | 902 |
|
| 903 | + /** |
| 904 | + * This customization adds client secret expiration time by setting {@code RegisteredClient.clientSecretExpiresAt} |
| 905 | + * during {@code OidcClientRegistration} -> {@code RegisteredClient} conversion |
| 906 | + */ |
| 907 | + private static final class ClientSecretExpirationRegisteredClientConverter |
| 908 | + implements Converter<OidcClientRegistration, RegisteredClient> { |
| 909 | + |
| 910 | + private static final OidcClientRegistrationRegisteredClientConverter delegate = |
| 911 | + new OidcClientRegistrationRegisteredClientConverter(); |
| 912 | + |
| 913 | + @Override |
| 914 | + public RegisteredClient convert(OidcClientRegistration clientRegistration) { |
| 915 | + RegisteredClient registeredClient = delegate.convert(clientRegistration); |
| 916 | + var registeredClientBuilder = RegisteredClient.from(registeredClient); |
| 917 | + |
| 918 | + var clientSecretExpiresAt = Instant.now().plus(Duration.ofHours(24)); |
| 919 | + registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt); |
| 920 | + |
| 921 | + return registeredClientBuilder.build(); |
| 922 | + } |
| 923 | + } |
| 924 | + |
817 | 925 | }
|
0 commit comments