feat: Allow configuration for token and refresh token time-to-live (#1853)

pull/1854/head
guqing 2022-04-19 11:16:15 +08:00 committed by GitHub
parent 9dd778bdab
commit eee58989fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 30 deletions

View File

@ -26,10 +26,6 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
"https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final OAuth2AuthorizationService authorizationService; private final OAuth2AuthorizationService authorizationService;
/**
* TODO from token settings
*/
private static final boolean isReuseRefreshTokens = false;
public JwtDaoAuthenticationProvider( public JwtDaoAuthenticationProvider(
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
@ -89,9 +85,12 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
authorizationBuilder.accessToken(accessToken); authorizationBuilder.accessToken(accessToken);
} }
ProviderSettings providerSettings =
ProviderContextHolder.getProviderContext().providerSettings();
// ----- Refresh token ----- // ----- Refresh token -----
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken(); OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
if (!isReuseRefreshTokens) { if (!providerSettings.isReuseRefreshTokens()) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (generatedRefreshToken == null) { if (generatedRefreshToken == null) {

View File

@ -1,7 +1,6 @@
package run.halo.app.identity.authentication; package run.halo.app.identity.authentication;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@ -21,11 +20,11 @@ import run.halo.app.infra.utils.HaloUtils;
* used for an {@link OAuth2AccessToken}. * used for an {@link OAuth2AccessToken}.
* *
* @author guqing * @author guqing
* @date 2022-04-14
* @see OAuth2TokenGenerator * @see OAuth2TokenGenerator
* @see Jwt * @see Jwt
* @see JwtEncoder * @see JwtEncoder
* @see OAuth2AccessToken * @see OAuth2AccessToken
* @since 2.0.0
*/ */
public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerator<Jwt> { public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerator<Jwt> {
/** /**
@ -46,9 +45,16 @@ public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerato
return null; return null;
} }
Instant issuedAt = Instant.now(); Instant issuedAt = Instant.now();
// TODO Allow configuration for ID Token time-to-live
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); ProviderSettings providerSettings = context.getProviderContext().providerSettings();
;
Instant expiresAt;
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
expiresAt = issuedAt.plus(providerSettings.getAccessTokenTimeToLive());
} else {
// refresh token
expiresAt = issuedAt.plus(providerSettings.getRefreshTokenTimeToLive());
}
String issuer = null; String issuer = null;
if (context.getProviderContext() != null) { if (context.getProviderContext() != null) {

View File

@ -10,7 +10,7 @@ import org.springframework.util.Assert;
/** /**
* @author guqing * @author guqing
* @date 2022-04-14 * @since 2.0.0
*/ */
public interface OAuth2TokenContext extends Context { public interface OAuth2TokenContext extends Context {

View File

@ -1,5 +1,6 @@
package run.halo.app.identity.authentication; package run.halo.app.identity.authentication;
import java.time.Duration;
import java.util.Map; import java.util.Map;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import run.halo.app.infra.config.AbstractSettings; import run.halo.app.infra.config.AbstractSettings;
@ -9,7 +10,7 @@ import run.halo.app.infra.config.ConfigurationSettingNames;
* A facility for provider configuration settings. * A facility for provider configuration settings.
* *
* @author guqing * @author guqing
* @date 2022-04-14 * @since 2.0.0
*/ */
public final class ProviderSettings extends AbstractSettings { public final class ProviderSettings extends AbstractSettings {
private ProviderSettings(Map<String, Object> settings) { private ProviderSettings(Map<String, Object> settings) {
@ -53,6 +54,32 @@ public final class ProviderSettings extends AbstractSettings {
return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT); return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT);
} }
/**
* Returns the time-to-live for an access token. The default is 5 minutes.
*
* @return the time-to-live for an access token
*/
public Duration getAccessTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE);
}
/**
* Returns {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued. The default is {@code true}.
*/
public boolean isReuseRefreshTokens() {
return getSetting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS);
}
/**
* Returns the time-to-live for a refresh token. The default is 60 minutes.
*
* @return the time-to-live for a refresh token
*/
public Duration getRefreshTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE);
}
/** /**
* Constructs a new {@link Builder} with the default settings. * Constructs a new {@link Builder} with the default settings.
* *
@ -60,9 +87,12 @@ public final class ProviderSettings extends AbstractSettings {
*/ */
public static Builder builder() { public static Builder builder() {
return new Builder() return new Builder()
.authorizationEndpoint("/oauth2/authorize") .authorizationEndpoint("/api/v1/oauth2/authorize")
.tokenEndpoint("/oauth2/token") .tokenEndpoint("/api/v1/oauth2/token")
.jwkSetEndpoint("/oauth2/jwks"); .jwkSetEndpoint("/api/v1/oauth2/jwks")
.accessTokenTimeToLive(Duration.ofMinutes(5L))
.refreshTokenTimeToLive(Duration.ofMinutes(60))
.reuseRefreshTokens(false);
} }
/** /**
@ -126,6 +156,48 @@ public final class ProviderSettings extends AbstractSettings {
return setting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT, jwkSetEndpoint); return setting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT, jwkSetEndpoint);
} }
/**
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
*
* @param accessTokenTimeToLive the time-to-live for an access token
* @return the {@link Builder} for further configuration
*/
public Builder accessTokenTimeToLive(Duration accessTokenTimeToLive) {
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0,
"accessTokenTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE,
accessTokenTimeToLive);
}
/**
* Set to {@code true} if refresh tokens are reused when returning the access token
* response,
* or {@code false} if a new refresh token is issued.
*
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue
* new refresh tokens
* @return the {@link Builder} for further configuration
*/
public Builder reuseRefreshTokens(boolean reuseRefreshTokens) {
return setting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS,
reuseRefreshTokens);
}
/**
* Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
*
* @param refreshTokenTimeToLive the time-to-live for a refresh token
* @return the {@link Builder} for further configuration
*/
public Builder refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0,
"refreshTokenTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE,
refreshTokenTimeToLive);
}
/** /**
* Builds the {@link ProviderSettings}. * Builds the {@link ProviderSettings}.
* *

View File

@ -12,7 +12,7 @@ import org.springframework.util.Assert;
* Base implementation for configuration settings. * Base implementation for configuration settings.
* *
* @author guqing * @author guqing
* @date 2022-04-14 * @since 2.0.0
*/ */
public abstract class AbstractSettings implements Serializable { public abstract class AbstractSettings implements Serializable {
private final Map<String, Object> settings; private final Map<String, Object> settings;

View File

@ -59,14 +59,6 @@ public class ConfigurationSettingNames {
public static final String ACCESS_TOKEN_TIME_TO_LIVE = public static final String ACCESS_TOKEN_TIME_TO_LIVE =
TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live"); TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live");
/**
* Set the {@link OAuth2TokenFormat token format} for an access token.
*
* @since 0.2.3
*/
public static final String ACCESS_TOKEN_FORMAT =
TOKEN_SETTINGS_NAMESPACE.concat("access-token-format");
/** /**
* Set to {@code true} if refresh tokens are reused when returning the access token * Set to {@code true} if refresh tokens are reused when returning the access token
* response, * response,

View File

@ -6,7 +6,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -100,7 +99,15 @@ public class JwtGeneratorTest {
tokenContext.getPrincipal().getName()); tokenContext.getPrincipal().getName());
Instant issuedAt = Instant.now(); Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
ProviderSettings providerSettings = tokenContext.getProviderContext().providerSettings();
Instant expiresAt;
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType())) {
expiresAt = issuedAt.plus(providerSettings.getAccessTokenTimeToLive());
} else {
expiresAt = issuedAt.plus(providerSettings.getAccessTokenTimeToLive());
}
assertThat(jwtClaimsSet.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), assertThat(jwtClaimsSet.getIssuedAt()).isBetween(issuedAt.minusSeconds(1),
issuedAt.plusSeconds(1)); issuedAt.plusSeconds(1));
assertThat(jwtClaimsSet.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), assertThat(jwtClaimsSet.getExpiresAt()).isBetween(expiresAt.minusSeconds(1),

View File

@ -3,6 +3,7 @@ package run.halo.app.authentication;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import java.time.Duration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import run.halo.app.identity.authentication.ProviderSettings; import run.halo.app.identity.authentication.ProviderSettings;
@ -19,9 +20,13 @@ public class ProviderSettingsTest {
ProviderSettings providerSettings = ProviderSettings.builder().build(); ProviderSettings providerSettings = ProviderSettings.builder().build();
assertThat(providerSettings.getIssuer()).isNull(); assertThat(providerSettings.getIssuer()).isNull();
assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo("/oauth2/authorize"); assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo(
assertThat(providerSettings.getTokenEndpoint()).isEqualTo("/oauth2/token"); "/api/v1/oauth2/authorize");
assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks"); assertThat(providerSettings.getTokenEndpoint()).isEqualTo("/api/v1/oauth2/token");
assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo("/api/v1/oauth2/jwks");
assertThat(providerSettings.isReuseRefreshTokens()).isEqualTo(false);
assertThat(providerSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
assertThat(providerSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
} }
@Test @Test
@ -30,18 +35,27 @@ public class ProviderSettingsTest {
String tokenEndpoint = "/oauth2/v1/token"; String tokenEndpoint = "/oauth2/v1/token";
String jwkSetEndpoint = "/oauth2/v1/jwks"; String jwkSetEndpoint = "/oauth2/v1/jwks";
String issuer = "https://example.com:9000"; String issuer = "https://example.com:9000";
boolean isReuseRefreshTokens = true;
Duration accessTokenTimeToLive = Duration.ofMinutes(30);
Duration refreshTokenTimeToLive = Duration.ofMinutes(120);
ProviderSettings providerSettings = ProviderSettings.builder() ProviderSettings providerSettings = ProviderSettings.builder()
.issuer(issuer) .issuer(issuer)
.authorizationEndpoint(authorizationEndpoint) .authorizationEndpoint(authorizationEndpoint)
.tokenEndpoint(tokenEndpoint) .tokenEndpoint(tokenEndpoint)
.jwkSetEndpoint(jwkSetEndpoint) .jwkSetEndpoint(jwkSetEndpoint)
.reuseRefreshTokens(isReuseRefreshTokens)
.accessTokenTimeToLive(accessTokenTimeToLive)
.refreshTokenTimeToLive(refreshTokenTimeToLive)
.build(); .build();
assertThat(providerSettings.getIssuer()).isEqualTo(issuer); assertThat(providerSettings.getIssuer()).isEqualTo(issuer);
assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo(authorizationEndpoint); assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo(authorizationEndpoint);
assertThat(providerSettings.getTokenEndpoint()).isEqualTo(tokenEndpoint); assertThat(providerSettings.getTokenEndpoint()).isEqualTo(tokenEndpoint);
assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint); assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
assertThat(providerSettings.isReuseRefreshTokens()).isEqualTo(isReuseRefreshTokens);
assertThat(providerSettings.getAccessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive);
assertThat(providerSettings.getRefreshTokenTimeToLive()).isEqualTo(refreshTokenTimeToLive);
} }
@Test @Test
@ -51,7 +65,7 @@ public class ProviderSettingsTest {
.settings(settings -> settings.put("name2", "value2")) .settings(settings -> settings.put("name2", "value2"))
.build(); .build();
assertThat(providerSettings.getSettings()).hasSize(5); assertThat(providerSettings.getSettings()).hasSize(8);
assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1"); assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2"); assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2");
} }
@ -83,4 +97,32 @@ public class ProviderSettingsTest {
.isThrownBy(() -> ProviderSettings.builder().jwkSetEndpoint(null)) .isThrownBy(() -> ProviderSettings.builder().jwkSetEndpoint(null))
.withMessage("value cannot be null"); .withMessage("value cannot be null");
} }
@Test
public void refreshTokenTimeToLiveWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ProviderSettings.builder().refreshTokenTimeToLive(null))
.withMessage("refreshTokenTimeToLive cannot be null");
}
@Test
public void refreshTokenTimeToLiveWhenLessThanZeroThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ProviderSettings.builder().refreshTokenTimeToLive(Duration.ZERO))
.withMessage("refreshTokenTimeToLive must be greater than Duration.ZERO");
}
@Test
public void accessTokenTimeToLiveToLiveWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ProviderSettings.builder().accessTokenTimeToLive(null))
.withMessage("accessTokenTimeToLive cannot be null");
}
@Test
public void accessTokenTimeToLiveToLiveWhenLessThanZeroThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ProviderSettings.builder().accessTokenTimeToLive(Duration.ZERO))
.withMessage("accessTokenTimeToLive must be greater than Duration.ZERO");
}
} }