diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java index bec0ae9e..1632b318 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java @@ -17,7 +17,6 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.security.Principal; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Collections; import java.util.HashMap; @@ -565,8 +564,10 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen !OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) { return null; } + RegisteredClient registeredClient = context.getRegisteredClient(); + Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live + Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAuthorizationCodeTimeToLive()); return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java index 5a85175e..24d9df27 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java @@ -128,6 +128,11 @@ public final class ConfigurationSettingNames { public static final class Token { private static final String TOKEN_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("token."); + /** + * Set the time-to-live for an authorization code. + */ + public static final String AUTHORIZATION_CODE_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("authorization-code-time-to-live"); + /** * Set the time-to-live for an access token. */ diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java index ba82fcf0..45d710da 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java @@ -37,6 +37,15 @@ public final class TokenSettings extends AbstractSettings { super(settings); } + /** + * Returns the time-to-live for an authorization code. The default is 5 minutes. + * + * @return the time-to-live for an authorization code + */ + public Duration getAuthorizationCodeTimeToLive() { + return getSetting(ConfigurationSettingNames.Token.AUTHORIZATION_CODE_TIME_TO_LIVE); + } + /** * Returns the time-to-live for an access token. The default is 5 minutes. * @@ -91,6 +100,7 @@ public final class TokenSettings extends AbstractSettings { */ public static Builder builder() { return new Builder() + .authorizationCodeTimeToLive(Duration.ofMinutes(5)) .accessTokenTimeToLive(Duration.ofMinutes(5)) .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) .reuseRefreshTokens(true) @@ -118,6 +128,19 @@ public final class TokenSettings extends AbstractSettings { private Builder() { } + /** + * Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}. + * A maximum authorization code lifetime of 10 minutes is RECOMMENDED + * + * @param authorizationCodeTimeToLive the time-to-live for an authorization code + * @return the {@link Builder} for further configuration + */ + public Builder authorizationCodeTimeToLive(Duration authorizationCodeTimeToLive) { + Assert.notNull(authorizationCodeTimeToLive, "authorizationCodeTimeToLive cannot be null"); + Assert.isTrue(authorizationCodeTimeToLive.getSeconds() > 0, "authorizationCodeTimeToLive must be greater than Duration.ZERO"); + return setting(ConfigurationSettingNames.Token.AUTHORIZATION_CODE_TIME_TO_LIVE, authorizationCodeTimeToLive); + } + /** * Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}. * diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java index f177977f..345af3f7 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java @@ -35,7 +35,8 @@ public class TokenSettingsTests { @Test public void buildWhenDefaultThenDefaultsAreSet() { TokenSettings tokenSettings = TokenSettings.builder().build(); - assertThat(tokenSettings.getSettings()).hasSize(5); + assertThat(tokenSettings.getSettings()).hasSize(6); + assertThat(tokenSettings.getAuthorizationCodeTimeToLive()).isEqualTo(Duration.ofMinutes(5)); assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5)); assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED); assertThat(tokenSettings.isReuseRefreshTokens()).isTrue(); @@ -43,6 +44,33 @@ public class TokenSettingsTests { assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256); } + @Test + public void authorizationCodeTimeToLiveWhenProvidedThenSet() { + Duration authorizationCodeTimeToLive = Duration.ofMinutes(10); + TokenSettings tokenSettings = TokenSettings.builder() + .authorizationCodeTimeToLive(authorizationCodeTimeToLive) + .build(); + assertThat(tokenSettings.getAuthorizationCodeTimeToLive()).isEqualTo(authorizationCodeTimeToLive); + } + + @Test + public void authorizationCodeTimeToLiveWhenNullOrZeroOrNegativeThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> TokenSettings.builder().authorizationCodeTimeToLive(null)) + .isInstanceOf(IllegalArgumentException.class) + .extracting(Throwable::getMessage) + .isEqualTo("authorizationCodeTimeToLive cannot be null"); + + assertThatThrownBy(() -> TokenSettings.builder().authorizationCodeTimeToLive(Duration.ZERO)) + .isInstanceOf(IllegalArgumentException.class) + .extracting(Throwable::getMessage) + .isEqualTo("authorizationCodeTimeToLive must be greater than Duration.ZERO"); + + assertThatThrownBy(() -> TokenSettings.builder().authorizationCodeTimeToLive(Duration.ofSeconds(-10))) + .isInstanceOf(IllegalArgumentException.class) + .extracting(Throwable::getMessage) + .isEqualTo("authorizationCodeTimeToLive must be greater than Duration.ZERO"); + } + @Test public void accessTokenTimeToLiveWhenProvidedThenSet() { Duration accessTokenTimeToLive = Duration.ofMinutes(10); @@ -136,7 +164,7 @@ public class TokenSettingsTests { .setting("name1", "value1") .settings(settings -> settings.put("name2", "value2")) .build(); - assertThat(tokenSettings.getSettings()).hasSize(7); + assertThat(tokenSettings.getSettings()).hasSize(8); assertThat(tokenSettings.getSetting("name1")).isEqualTo("value1"); assertThat(tokenSettings.getSetting("name2")).isEqualTo("value2"); }