mirror of https://github.com/halo-dev/halo
feat: Allow configuration for token and refresh token time-to-live (#1853)
parent
9dd778bdab
commit
eee58989fe
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import org.springframework.util.Assert;
|
|||
|
||||
/**
|
||||
* @author guqing
|
||||
* @date 2022-04-14
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface OAuth2TokenContext extends Context {
|
||||
|
||||
|
|
|
@ -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}.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue