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";
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final OAuth2AuthorizationService authorizationService;
/**
* TODO from token settings
*/
private static final boolean isReuseRefreshTokens = false;
public JwtDaoAuthenticationProvider(
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
@ -89,9 +85,12 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
authorizationBuilder.accessToken(accessToken);
}
ProviderSettings providerSettings =
ProviderContextHolder.getProviderContext().providerSettings();
// ----- Refresh token -----
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
if (!isReuseRefreshTokens) {
if (!providerSettings.isReuseRefreshTokens()) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (generatedRefreshToken == null) {

View File

@ -1,7 +1,6 @@
package run.halo.app.identity.authentication;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@ -21,11 +20,11 @@ import run.halo.app.infra.utils.HaloUtils;
* used for an {@link OAuth2AccessToken}.
*
* @author guqing
* @date 2022-04-14
* @see OAuth2TokenGenerator
* @see Jwt
* @see JwtEncoder
* @see OAuth2AccessToken
* @since 2.0.0
*/
public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerator<Jwt> {
/**
@ -46,9 +45,16 @@ public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerato
return null;
}
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;
if (context.getProviderContext() != null) {

View File

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

View File

@ -1,5 +1,6 @@
package run.halo.app.identity.authentication;
import java.time.Duration;
import java.util.Map;
import org.springframework.util.Assert;
import run.halo.app.infra.config.AbstractSettings;
@ -9,7 +10,7 @@ import run.halo.app.infra.config.ConfigurationSettingNames;
* A facility for provider configuration settings.
*
* @author guqing
* @date 2022-04-14
* @since 2.0.0
*/
public final class ProviderSettings extends AbstractSettings {
private ProviderSettings(Map<String, Object> settings) {
@ -53,6 +54,32 @@ public final class ProviderSettings extends AbstractSettings {
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.
*
@ -60,9 +87,12 @@ public final class ProviderSettings extends AbstractSettings {
*/
public static Builder builder() {
return new Builder()
.authorizationEndpoint("/oauth2/authorize")
.tokenEndpoint("/oauth2/token")
.jwkSetEndpoint("/oauth2/jwks");
.authorizationEndpoint("/api/v1/oauth2/authorize")
.tokenEndpoint("/api/v1/oauth2/token")
.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);
}
/**
* 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}.
*

View File

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

View File

@ -59,14 +59,6 @@ public class ConfigurationSettingNames {
public static final String 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
* response,

View File

@ -6,7 +6,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
@ -100,7 +99,15 @@ public class JwtGeneratorTest {
tokenContext.getPrincipal().getName());
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),
issuedAt.plusSeconds(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.assertThatIllegalArgumentException;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import run.halo.app.identity.authentication.ProviderSettings;
@ -19,9 +20,13 @@ public class ProviderSettingsTest {
ProviderSettings providerSettings = ProviderSettings.builder().build();
assertThat(providerSettings.getIssuer()).isNull();
assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo("/oauth2/authorize");
assertThat(providerSettings.getTokenEndpoint()).isEqualTo("/oauth2/token");
assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo(
"/api/v1/oauth2/authorize");
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
@ -30,18 +35,27 @@ public class ProviderSettingsTest {
String tokenEndpoint = "/oauth2/v1/token";
String jwkSetEndpoint = "/oauth2/v1/jwks";
String issuer = "https://example.com:9000";
boolean isReuseRefreshTokens = true;
Duration accessTokenTimeToLive = Duration.ofMinutes(30);
Duration refreshTokenTimeToLive = Duration.ofMinutes(120);
ProviderSettings providerSettings = ProviderSettings.builder()
.issuer(issuer)
.authorizationEndpoint(authorizationEndpoint)
.tokenEndpoint(tokenEndpoint)
.jwkSetEndpoint(jwkSetEndpoint)
.reuseRefreshTokens(isReuseRefreshTokens)
.accessTokenTimeToLive(accessTokenTimeToLive)
.refreshTokenTimeToLive(refreshTokenTimeToLive)
.build();
assertThat(providerSettings.getIssuer()).isEqualTo(issuer);
assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo(authorizationEndpoint);
assertThat(providerSettings.getTokenEndpoint()).isEqualTo(tokenEndpoint);
assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
assertThat(providerSettings.isReuseRefreshTokens()).isEqualTo(isReuseRefreshTokens);
assertThat(providerSettings.getAccessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive);
assertThat(providerSettings.getRefreshTokenTimeToLive()).isEqualTo(refreshTokenTimeToLive);
}
@Test
@ -51,7 +65,7 @@ public class ProviderSettingsTest {
.settings(settings -> settings.put("name2", "value2"))
.build();
assertThat(providerSettings.getSettings()).hasSize(5);
assertThat(providerSettings.getSettings()).hasSize(8);
assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2");
}
@ -83,4 +97,32 @@ public class ProviderSettingsTest {
.isThrownBy(() -> ProviderSettings.builder().jwkSetEndpoint(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");
}
}