mirror of https://github.com/halo-dev/halo
feat: Add JwtGenerator for authentication (#1843)
* feat: Add JwtGenerator for authentication * fix: code style * refactor: change OAuth2TokenType type to record * refactor: remove explicit declaration of equals and hashcode for records classpull/1846/head
parent
cec6897574
commit
20e6d4d1eb
|
@ -50,7 +50,6 @@ public class WebSecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
// @formatter:off
|
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests((authorize) -> authorize
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
.antMatchers("/api/**", "/apis/**").authenticated()
|
.antMatchers("/api/**", "/apis/**").authenticated()
|
||||||
|
@ -63,20 +62,17 @@ public class WebSecurityConfig {
|
||||||
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
|
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
|
||||||
.accessDeniedHandler(new JwtAccessDeniedHandler())
|
.accessDeniedHandler(new JwtAccessDeniedHandler())
|
||||||
);
|
);
|
||||||
// @formatter:on
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
UserDetailsService users() {
|
UserDetailsService users() {
|
||||||
// @formatter:off
|
|
||||||
return new InMemoryUserDetailsManager(
|
return new InMemoryUserDetailsManager(
|
||||||
User.withUsername("user")
|
User.withUsername("user")
|
||||||
.password("{noop}password")
|
.password("{noop}password")
|
||||||
.authorities("app")
|
.authorities("app")
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
// @formatter:on
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A facility for holding information associated to a specific context.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public interface Context {
|
||||||
|
/**
|
||||||
|
* Returns the value of the attribute associated to the key.
|
||||||
|
*
|
||||||
|
* @param key the key for the attribute
|
||||||
|
* @param <V> the type of the value for the attribute
|
||||||
|
* @return the value of the attribute associated to the key, or {@code null} if not available
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
<V> V get(Object key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the attribute associated to the key.
|
||||||
|
*
|
||||||
|
* @param key the key for the attribute
|
||||||
|
* @param <V> the type of the value for the attribute
|
||||||
|
* @return the value of the attribute associated to the key, or {@code null} if not available
|
||||||
|
* or not of the specified type
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
default <V> V get(Class<V> key) {
|
||||||
|
Assert.notNull(key, "key cannot be null");
|
||||||
|
V value = get((Object) key);
|
||||||
|
return key.isInstance(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if an attribute associated to the key exists, {@code false} otherwise.
|
||||||
|
*
|
||||||
|
* @param key the key for the attribute
|
||||||
|
* @return {@code true} if an attribute associated to the key exists, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
boolean hasKey(Object key);
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link OAuth2TokenContext}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public record DefaultOAuth2TokenContext(Map<Object, Object> context) implements OAuth2TokenContext {
|
||||||
|
public DefaultOAuth2TokenContext {
|
||||||
|
context = Map.copyOf(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <V> V get(Object key) {
|
||||||
|
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasKey(Object key) {
|
||||||
|
Assert.notNull(key, "key cannot be null");
|
||||||
|
return this.context.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link Builder}.
|
||||||
|
*
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for {@link DefaultOAuth2TokenContext}.
|
||||||
|
*/
|
||||||
|
public static final class Builder extends AbstractBuilder<DefaultOAuth2TokenContext, Builder> {
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link DefaultOAuth2TokenContext}.
|
||||||
|
*
|
||||||
|
* @return the {@link DefaultOAuth2TokenContext}
|
||||||
|
*/
|
||||||
|
public DefaultOAuth2TokenContext build() {
|
||||||
|
return new DefaultOAuth2TokenContext(getContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import run.halo.app.infra.utils.HaloUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link OAuth2TokenGenerator} that generates a {@link Jwt}
|
||||||
|
* used for an {@link OAuth2AccessToken}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
* @see OAuth2TokenGenerator
|
||||||
|
* @see Jwt
|
||||||
|
* @see JwtEncoder
|
||||||
|
* @see OAuth2AccessToken
|
||||||
|
*/
|
||||||
|
public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerator<Jwt> {
|
||||||
|
/**
|
||||||
|
* Constructs a {@code JwtGenerator} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param jwtEncoder the jwt encoder
|
||||||
|
*/
|
||||||
|
public JwtGenerator {
|
||||||
|
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Jwt generate(OAuth2TokenContext context) {
|
||||||
|
if (context.getTokenType() == null
|
||||||
|
|| !OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Instant issuedAt = Instant.now();
|
||||||
|
// TODO Allow configuration for ID Token time-to-live
|
||||||
|
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
|
||||||
|
;
|
||||||
|
|
||||||
|
String issuer = null;
|
||||||
|
if (context.getProviderContext() != null) {
|
||||||
|
issuer = context.getProviderContext().getIssuer();
|
||||||
|
}
|
||||||
|
|
||||||
|
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
|
||||||
|
if (StringUtils.hasText(issuer)) {
|
||||||
|
claimsBuilder.issuer(issuer);
|
||||||
|
}
|
||||||
|
claimsBuilder
|
||||||
|
.subject(context.getPrincipal().getName())
|
||||||
|
.issuedAt(issuedAt)
|
||||||
|
.notBefore(issuedAt)
|
||||||
|
.id(HaloUtils.simpleUUID())
|
||||||
|
.expiresAt(expiresAt);
|
||||||
|
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||||
|
claimsBuilder.notBefore(issuedAt);
|
||||||
|
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
|
||||||
|
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JwsHeader headers = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||||
|
return this.jwtEncoder.encode(JwtEncoderParameters.from(headers, claimsBuilder.build()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public interface OAuth2TokenContext extends Context {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link Authentication} representing the {@code Principal} resource owner (or
|
||||||
|
* client).
|
||||||
|
*
|
||||||
|
* @param <T> the type of the {@code Authentication}
|
||||||
|
* @return the {@link Authentication} representing the {@code Principal} resource owner (or
|
||||||
|
* client)
|
||||||
|
*/
|
||||||
|
default <T extends Authentication> T getPrincipal() {
|
||||||
|
return get(AbstractBuilder.PRINCIPAL_AUTHENTICATION_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link ProviderContext provider context}.
|
||||||
|
*
|
||||||
|
* @return the {@link ProviderContext}
|
||||||
|
* @since 0.2.3
|
||||||
|
*/
|
||||||
|
default ProviderContext getProviderContext() {
|
||||||
|
return get(ProviderContext.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authorized scope(s).
|
||||||
|
*
|
||||||
|
* @return the authorized scope(s)
|
||||||
|
*/
|
||||||
|
default Set<String> getAuthorizedScopes() {
|
||||||
|
return hasKey(AbstractBuilder.AUTHORIZATION_SCOPE_AUTHENTICATION_KEY)
|
||||||
|
? get(AbstractBuilder.AUTHORIZATION_SCOPE_AUTHENTICATION_KEY) :
|
||||||
|
Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link OAuth2TokenType token type}.
|
||||||
|
*
|
||||||
|
* @return the {@link OAuth2TokenType}
|
||||||
|
*/
|
||||||
|
default OAuth2TokenType getTokenType() {
|
||||||
|
return get(OAuth2TokenType.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base builder for implementations of {@link OAuth2TokenContext}.
|
||||||
|
*
|
||||||
|
* @param <T> the type of the context
|
||||||
|
* @param <B> the type of the builder
|
||||||
|
*/
|
||||||
|
abstract class AbstractBuilder<T extends OAuth2TokenContext, B extends AbstractBuilder<T, B>> {
|
||||||
|
private static final String PRINCIPAL_AUTHENTICATION_KEY =
|
||||||
|
Authentication.class.getName().concat(".PRINCIPAL");
|
||||||
|
private static final String AUTHORIZATION_SCOPE_AUTHENTICATION_KEY =
|
||||||
|
Authentication.class.getName().concat(".AUTHORIZATION_SCOPE");
|
||||||
|
private final Map<Object, Object> context = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Authentication} representing the {@code Principal} resource owner (or
|
||||||
|
* client).
|
||||||
|
*
|
||||||
|
* @param principal the {@link Authentication} representing the {@code Principal}
|
||||||
|
* resource owner (or client)
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B principal(Authentication principal) {
|
||||||
|
return put(PRINCIPAL_AUTHENTICATION_KEY, principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link ProviderContext provider context}.
|
||||||
|
*
|
||||||
|
* @param providerContext the {@link ProviderContext}
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
* @since 0.2.3
|
||||||
|
*/
|
||||||
|
public B providerContext(ProviderContext providerContext) {
|
||||||
|
return put(ProviderContext.class, providerContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the authorized scope(s).
|
||||||
|
*
|
||||||
|
* @param authorizedScopes the authorized scope(s)
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B authorizedScopes(Set<String> authorizedScopes) {
|
||||||
|
return put(AUTHORIZATION_SCOPE_AUTHENTICATION_KEY, authorizedScopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link OAuth2TokenType token type}.
|
||||||
|
*
|
||||||
|
* @param tokenType the {@link OAuth2TokenType}
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B tokenType(OAuth2TokenType tokenType) {
|
||||||
|
return put(OAuth2TokenType.class, tokenType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates an attribute.
|
||||||
|
*
|
||||||
|
* @param key the key for the attribute
|
||||||
|
* @param value the value of the attribute
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B put(Object key, Object value) {
|
||||||
|
Assert.notNull(key, "key cannot be null");
|
||||||
|
Assert.notNull(value, "value cannot be null");
|
||||||
|
this.context.put(key, value);
|
||||||
|
return getThis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code Consumer} of the attributes {@code Map}
|
||||||
|
* allowing the ability to add, replace, or remove.
|
||||||
|
*
|
||||||
|
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B context(Consumer<Map<Object, Object>> contextConsumer) {
|
||||||
|
contextConsumer.accept(this.context);
|
||||||
|
return getThis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <V> V get(Object key) {
|
||||||
|
return (V) this.context.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<Object, Object> getContext() {
|
||||||
|
return this.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected final B getThis() {
|
||||||
|
return (B) this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link OAuth2TokenContext}.
|
||||||
|
*
|
||||||
|
* @return the {@link OAuth2TokenContext}
|
||||||
|
*/
|
||||||
|
public abstract T build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementations of this interface are responsible for generating an {@link OAuth2Token}
|
||||||
|
* using the attributes contained in the {@link OAuth2TokenContext}.
|
||||||
|
*
|
||||||
|
* @param <T> the type of the OAuth 2.0 Token
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
* @see OAuth2Token
|
||||||
|
* @see OAuth2TokenContext
|
||||||
|
* @see ClaimAccessor
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface OAuth2TokenGenerator<T extends OAuth2Token> {
|
||||||
|
/**
|
||||||
|
* Generate an OAuth 2.0 Token using the attributes contained in the {@link OAuth2TokenContext},
|
||||||
|
* or return {@code null} if the {@link OAuth2TokenContext#getTokenType()} is not supported.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the returned {@link OAuth2Token} has a set of claims, it should implement
|
||||||
|
* {@link ClaimAccessor}
|
||||||
|
* in order for it to be stored with the {@link OAuth2Authorization}.
|
||||||
|
*
|
||||||
|
* @param context the context containing the OAuth 2.0 Token attributes
|
||||||
|
* @return an {@link OAuth2Token} or {@code null} if the
|
||||||
|
* {@link OAuth2TokenContext#getTokenType()} is not supported
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
T generate(OAuth2TokenContext context);
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public record OAuth2TokenType(String value) implements Serializable {
|
||||||
|
public static final OAuth2TokenType ACCESS_TOKEN = new OAuth2TokenType("access_token");
|
||||||
|
public static final OAuth2TokenType REFRESH_TOKEN = new OAuth2TokenType("refresh_token");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code OAuth2TokenType} using the provided value.
|
||||||
|
*
|
||||||
|
* @param value the value of the token type
|
||||||
|
*/
|
||||||
|
public OAuth2TokenType {
|
||||||
|
Assert.hasText(value, "value cannot be empty");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context that holds information of the Provider.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public record ProviderContext(ProviderSettings providerSettings,
|
||||||
|
@Nullable Supplier<String> issuerSupplier) {
|
||||||
|
/**
|
||||||
|
* Constructs a {@code ProviderContext} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param providerSettings the provider settings
|
||||||
|
* @param issuerSupplier a {@code Supplier} for the {@code URL} of the Provider's issuer
|
||||||
|
* identifier
|
||||||
|
*/
|
||||||
|
public ProviderContext {
|
||||||
|
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link ProviderSettings}.
|
||||||
|
*
|
||||||
|
* @return the {@link ProviderSettings}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ProviderSettings providerSettings() {
|
||||||
|
return this.providerSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@code URL} of the Provider's issuer identifier.
|
||||||
|
* The issuer identifier is resolved from the constructor parameter {@code Supplier<String>}
|
||||||
|
* or if not provided then defaults to {@link ProviderSettings#getIssuer()}.
|
||||||
|
*
|
||||||
|
* @return the {@code URL} of the Provider's issuer identifier
|
||||||
|
*/
|
||||||
|
public String getIssuer() {
|
||||||
|
return this.issuerSupplier != null
|
||||||
|
? this.issuerSupplier.get() :
|
||||||
|
providerSettings().getIssuer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.infra.config.AbstractSettings;
|
||||||
|
import run.halo.app.infra.config.ConfigurationSettingNames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A facility for provider configuration settings.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public final class ProviderSettings extends AbstractSettings {
|
||||||
|
private ProviderSettings(Map<String, Object> settings) {
|
||||||
|
super(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL of the Provider's Issuer Identifier
|
||||||
|
*
|
||||||
|
* @return the URL of the Provider's Issuer Identifier
|
||||||
|
*/
|
||||||
|
public String getIssuer() {
|
||||||
|
return getSetting(ConfigurationSettingNames.Provider.ISSUER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2
|
||||||
|
* /authorize}.
|
||||||
|
*
|
||||||
|
* @return the Authorization endpoint
|
||||||
|
*/
|
||||||
|
public String getAuthorizationEndpoint() {
|
||||||
|
return getSetting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
|
||||||
|
*
|
||||||
|
* @return the Token endpoint
|
||||||
|
*/
|
||||||
|
public String getTokenEndpoint() {
|
||||||
|
return getSetting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Provider's JWK Set endpoint. The default is {@code /oauth2/jwks}.
|
||||||
|
*
|
||||||
|
* @return the JWK Set endpoint
|
||||||
|
*/
|
||||||
|
public String getJwkSetEndpoint() {
|
||||||
|
return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link Builder} with the default settings.
|
||||||
|
*
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder()
|
||||||
|
.authorizationEndpoint("/oauth2/authorize")
|
||||||
|
.tokenEndpoint("/oauth2/token")
|
||||||
|
.jwkSetEndpoint("/oauth2/jwks");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link Builder} with the provided settings.
|
||||||
|
*
|
||||||
|
* @param settings the settings to initialize the builder
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public static Builder withSettings(Map<String, Object> settings) {
|
||||||
|
Assert.notEmpty(settings, "settings cannot be empty");
|
||||||
|
return new Builder()
|
||||||
|
.settings(s -> s.putAll(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for {@link ProviderSettings}.
|
||||||
|
*/
|
||||||
|
public static class Builder extends AbstractBuilder<ProviderSettings, Builder> {
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the URL the Provider uses as its Issuer Identifier.
|
||||||
|
*
|
||||||
|
* @param issuer the URL the Provider uses as its Issuer Identifier.
|
||||||
|
* @return the {@link Builder} for further configuration
|
||||||
|
*/
|
||||||
|
public Builder issuer(String issuer) {
|
||||||
|
return setting(ConfigurationSettingNames.Provider.ISSUER, issuer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Provider's OAuth 2.0 Authorization endpoint.
|
||||||
|
*
|
||||||
|
* @param authorizationEndpoint the Authorization endpoint
|
||||||
|
* @return the {@link Builder} for further configuration
|
||||||
|
*/
|
||||||
|
public Builder authorizationEndpoint(String authorizationEndpoint) {
|
||||||
|
return setting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT,
|
||||||
|
authorizationEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Provider's OAuth 2.0 Token endpoint.
|
||||||
|
*
|
||||||
|
* @param tokenEndpoint the Token endpoint
|
||||||
|
* @return the {@link Builder} for further configuration
|
||||||
|
*/
|
||||||
|
public Builder tokenEndpoint(String tokenEndpoint) {
|
||||||
|
return setting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT, tokenEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Provider's JWK Set endpoint.
|
||||||
|
*
|
||||||
|
* @param jwkSetEndpoint the JWK Set endpoint
|
||||||
|
* @return the {@link Builder} for further configuration
|
||||||
|
*/
|
||||||
|
public Builder jwkSetEndpoint(String jwkSetEndpoint) {
|
||||||
|
return setting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT, jwkSetEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@link ProviderSettings}.
|
||||||
|
*
|
||||||
|
* @return the {@link ProviderSettings}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ProviderSettings build() {
|
||||||
|
return new ProviderSettings(getSettings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package run.halo.app.infra.config;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation for configuration settings.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public abstract class AbstractSettings implements Serializable {
|
||||||
|
private final Map<String, Object> settings;
|
||||||
|
|
||||||
|
protected AbstractSettings(Map<String, Object> settings) {
|
||||||
|
Assert.notEmpty(settings, "settings cannot be empty");
|
||||||
|
this.settings = Collections.unmodifiableMap(new HashMap<>(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a configuration setting.
|
||||||
|
*
|
||||||
|
* @param name the name of the setting
|
||||||
|
* @param <T> the type of the setting
|
||||||
|
* @return the value of the setting, or {@code null} if not available
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getSetting(String name) {
|
||||||
|
Assert.hasText(name, "name cannot be empty");
|
||||||
|
return (T) getSettings().get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code Map} of the configuration settings.
|
||||||
|
*
|
||||||
|
* @return a {@code Map} of the configuration settings
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getSettings() {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AbstractSettings that = (AbstractSettings) obj;
|
||||||
|
return this.settings.equals(that.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AbstractSettings {" + "settings=" + this.settings + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for subclasses of {@link AbstractSettings}.
|
||||||
|
*/
|
||||||
|
protected abstract static class AbstractBuilder<T extends AbstractSettings,
|
||||||
|
B extends AbstractBuilder<T, B>> {
|
||||||
|
private final Map<String, Object> settings = new HashMap<>();
|
||||||
|
|
||||||
|
protected AbstractBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a configuration setting.
|
||||||
|
*
|
||||||
|
* @param name the name of the setting
|
||||||
|
* @param value the value of the setting
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B setting(String name, Object value) {
|
||||||
|
Assert.hasText(name, "name cannot be empty");
|
||||||
|
Assert.notNull(value, "value cannot be null");
|
||||||
|
getSettings().put(name, value);
|
||||||
|
return getThis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code Consumer} of the configuration settings {@code Map}
|
||||||
|
* allowing the ability to add, replace, or remove.
|
||||||
|
*
|
||||||
|
* @param settingsConsumer a {@link Consumer} of the configuration settings {@code Map}
|
||||||
|
* @return the {@link AbstractBuilder} for further configuration
|
||||||
|
*/
|
||||||
|
public B settings(Consumer<Map<String, Object>> settingsConsumer) {
|
||||||
|
settingsConsumer.accept(getSettings());
|
||||||
|
return getThis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract T build();
|
||||||
|
|
||||||
|
protected final Map<String, Object> getSettings() {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected final B getThis() {
|
||||||
|
return (B) this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package run.halo.app.infra.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names for all the configuration settings.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public class ConfigurationSettingNames {
|
||||||
|
private static final String SETTINGS_NAMESPACE = "settings.";
|
||||||
|
|
||||||
|
private ConfigurationSettingNames() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names for provider configuration settings.
|
||||||
|
*/
|
||||||
|
public static final class Provider {
|
||||||
|
private static final String PROVIDER_SETTINGS_NAMESPACE =
|
||||||
|
SETTINGS_NAMESPACE.concat("provider.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the URL the Provider uses as its Issuer Identifier.
|
||||||
|
*/
|
||||||
|
public static final String ISSUER = PROVIDER_SETTINGS_NAMESPACE.concat("issuer");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Provider's OAuth 2.0 Authorization endpoint.
|
||||||
|
*/
|
||||||
|
public static final String AUTHORIZATION_ENDPOINT =
|
||||||
|
PROVIDER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Provider's OAuth 2.0 Token endpoint.
|
||||||
|
*/
|
||||||
|
public static final String TOKEN_ENDPOINT =
|
||||||
|
PROVIDER_SETTINGS_NAMESPACE.concat("token-endpoint");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Provider's JWK Set endpoint.
|
||||||
|
*/
|
||||||
|
public static final String JWK_SET_ENDPOINT =
|
||||||
|
PROVIDER_SETTINGS_NAMESPACE.concat("jwk-set-endpoint");
|
||||||
|
|
||||||
|
private Provider() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names for token configuration settings.
|
||||||
|
*/
|
||||||
|
public static final class Token {
|
||||||
|
private static final String TOKEN_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("token.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the time-to-live for an access token.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
* or {@code false} if a new refresh token is issued.
|
||||||
|
*/
|
||||||
|
public static final String REUSE_REFRESH_TOKENS =
|
||||||
|
TOKEN_SETTINGS_NAMESPACE.concat("reuse-refresh-tokens");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the time-to-live for a refresh token.
|
||||||
|
*/
|
||||||
|
public static final String REFRESH_TOKEN_TIME_TO_LIVE =
|
||||||
|
TOKEN_SETTINGS_NAMESPACE.concat("refresh-token-time-to-live");
|
||||||
|
|
||||||
|
private Token() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package run.halo.app.authentication;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JwtClaimsSet}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public class JwtClaimsSetTest {
|
||||||
|
|
||||||
|
public static JwtClaimsSet.Builder jwtClaimsSet() {
|
||||||
|
String issuer = "https://provider.com";
|
||||||
|
Instant issuedAt = Instant.now();
|
||||||
|
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
|
||||||
|
|
||||||
|
return JwtClaimsSet.builder()
|
||||||
|
.issuer(issuer)
|
||||||
|
.subject("subject")
|
||||||
|
.audience(Collections.singletonList("client-1"))
|
||||||
|
.issuedAt(issuedAt)
|
||||||
|
.notBefore(issuedAt)
|
||||||
|
.expiresAt(expiresAt)
|
||||||
|
.id("jti")
|
||||||
|
.claim("custom-claim-name", "custom-claim-value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildWhenClaimsEmptyThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> JwtClaimsSet.builder().build())
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("claims cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildWhenAllClaimsProvidedThenAllClaimsAreSet() {
|
||||||
|
JwtClaimsSet expectedJwtClaimsSet = jwtClaimsSet().build();
|
||||||
|
|
||||||
|
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
|
||||||
|
.issuer(expectedJwtClaimsSet.getIssuer().toExternalForm())
|
||||||
|
.subject(expectedJwtClaimsSet.getSubject())
|
||||||
|
.audience(expectedJwtClaimsSet.getAudience())
|
||||||
|
.issuedAt(expectedJwtClaimsSet.getIssuedAt())
|
||||||
|
.notBefore(expectedJwtClaimsSet.getNotBefore())
|
||||||
|
.expiresAt(expectedJwtClaimsSet.getExpiresAt())
|
||||||
|
.id(expectedJwtClaimsSet.getId())
|
||||||
|
.claims(claims -> claims.put("custom-claim-name", "custom-claim-value"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(jwtClaimsSet.getIssuer()).isEqualTo(expectedJwtClaimsSet.getIssuer());
|
||||||
|
assertThat(jwtClaimsSet.getSubject()).isEqualTo(expectedJwtClaimsSet.getSubject());
|
||||||
|
assertThat(jwtClaimsSet.getAudience()).isEqualTo(expectedJwtClaimsSet.getAudience());
|
||||||
|
assertThat(jwtClaimsSet.getIssuedAt()).isEqualTo(expectedJwtClaimsSet.getIssuedAt());
|
||||||
|
assertThat(jwtClaimsSet.getNotBefore()).isEqualTo(expectedJwtClaimsSet.getNotBefore());
|
||||||
|
assertThat(jwtClaimsSet.getExpiresAt()).isEqualTo(expectedJwtClaimsSet.getExpiresAt());
|
||||||
|
assertThat(jwtClaimsSet.getId()).isEqualTo(expectedJwtClaimsSet.getId());
|
||||||
|
assertThat(jwtClaimsSet.<String>getClaim("custom-claim-name")).isEqualTo(
|
||||||
|
"custom-claim-value");
|
||||||
|
assertThat(jwtClaimsSet.getClaims()).isEqualTo(expectedJwtClaimsSet.getClaims());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> JwtClaimsSet.from(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("claims cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromWhenClaimsProvidedThenCopied() {
|
||||||
|
JwtClaimsSet expectedJwtClaimsSet = jwtClaimsSet().build();
|
||||||
|
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.from(expectedJwtClaimsSet).build();
|
||||||
|
assertThat(jwtClaimsSet.getClaims()).isEqualTo(expectedJwtClaimsSet.getClaims());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void claimWhenNameNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> JwtClaimsSet.builder().claim(null, "value"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("name cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void claimWhenValueNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> JwtClaimsSet.builder().claim("name", null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("value cannot be null");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package run.halo.app.authentication;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
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;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import run.halo.app.identity.authentication.DefaultOAuth2TokenContext;
|
||||||
|
import run.halo.app.identity.authentication.JwtGenerator;
|
||||||
|
import run.halo.app.identity.authentication.OAuth2TokenContext;
|
||||||
|
import run.halo.app.identity.authentication.OAuth2TokenType;
|
||||||
|
import run.halo.app.identity.authentication.ProviderContext;
|
||||||
|
import run.halo.app.identity.authentication.ProviderSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JwtGenerator}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public class JwtGeneratorTest {
|
||||||
|
private JwtEncoder jwtEncoder;
|
||||||
|
private JwtGenerator jwtGenerator;
|
||||||
|
private ProviderContext providerContext;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
this.jwtEncoder = mock(JwtEncoder.class);
|
||||||
|
this.jwtGenerator = new JwtGenerator(this.jwtEncoder);
|
||||||
|
ProviderSettings
|
||||||
|
providerSettings = ProviderSettings.builder().issuer("https://provider.com").build();
|
||||||
|
this.providerContext = new ProviderContext(providerSettings, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorWhenJwtEncoderNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> new JwtGenerator(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("jwtEncoder cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateWhenUnsupportedTokenTypeThenReturnNull() {
|
||||||
|
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
|
||||||
|
.tokenType(new OAuth2TokenType("unsupported_token_type"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(this.jwtGenerator.generate(tokenContext)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateWhenAccessTokenTypeThenReturnJwt() {
|
||||||
|
TestingAuthenticationToken authentication =
|
||||||
|
new TestingAuthenticationToken("userPrincipal", "123456", "ROLE_USER");
|
||||||
|
Set<String> scope = authentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
|
||||||
|
.principal(authentication)
|
||||||
|
.providerContext(this.providerContext)
|
||||||
|
.authorizedScopes(scope)
|
||||||
|
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertGeneratedTokenType(tokenContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertGeneratedTokenType(OAuth2TokenContext tokenContext) {
|
||||||
|
this.jwtGenerator.generate(tokenContext);
|
||||||
|
|
||||||
|
ArgumentCaptor<JwtEncoderParameters> jwtEncoderParametersArgumentCaptor =
|
||||||
|
ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
||||||
|
verify(this.jwtEncoder).encode(jwtEncoderParametersArgumentCaptor.capture());
|
||||||
|
|
||||||
|
JwtEncoderParameters encoderParameters = jwtEncoderParametersArgumentCaptor.getValue();
|
||||||
|
JwsHeader jwsHeader = encoderParameters.getJwsHeader();
|
||||||
|
assertThat(jwsHeader).isNotNull();
|
||||||
|
assertThat(jwsHeader.getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
|
||||||
|
|
||||||
|
JwtClaimsSet jwtClaimsSet = encoderParameters.getClaims();
|
||||||
|
assertThat(jwtClaimsSet.getIssuer().toExternalForm()).isEqualTo(
|
||||||
|
tokenContext.getProviderContext().getIssuer());
|
||||||
|
assertThat(jwtClaimsSet.getSubject()).isEqualTo(
|
||||||
|
tokenContext.getPrincipal().getName());
|
||||||
|
|
||||||
|
Instant issuedAt = Instant.now();
|
||||||
|
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
|
||||||
|
assertThat(jwtClaimsSet.getIssuedAt()).isBetween(issuedAt.minusSeconds(1),
|
||||||
|
issuedAt.plusSeconds(1));
|
||||||
|
assertThat(jwtClaimsSet.getExpiresAt()).isBetween(expiresAt.minusSeconds(1),
|
||||||
|
expiresAt.plusSeconds(1));
|
||||||
|
|
||||||
|
if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
|
||||||
|
assertThat(jwtClaimsSet.getNotBefore()).isBetween(issuedAt.minusSeconds(1),
|
||||||
|
issuedAt.plusSeconds(1));
|
||||||
|
|
||||||
|
Set<String> scopes = jwtClaimsSet.getClaim(OAuth2ParameterNames.SCOPE);
|
||||||
|
assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package run.halo.app.authentication;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import run.halo.app.identity.authentication.ProviderSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ProviderSettings}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @date 2022-04-14
|
||||||
|
*/
|
||||||
|
public class ProviderSettingsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildWhenDefaultThenDefaultsAreSet() {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildWhenSettingsProvidedThenSet() {
|
||||||
|
String authorizationEndpoint = "/oauth2/v1/authorize";
|
||||||
|
String tokenEndpoint = "/oauth2/v1/token";
|
||||||
|
String jwkSetEndpoint = "/oauth2/v1/jwks";
|
||||||
|
String issuer = "https://example.com:9000";
|
||||||
|
|
||||||
|
ProviderSettings providerSettings = ProviderSettings.builder()
|
||||||
|
.issuer(issuer)
|
||||||
|
.authorizationEndpoint(authorizationEndpoint)
|
||||||
|
.tokenEndpoint(tokenEndpoint)
|
||||||
|
.jwkSetEndpoint(jwkSetEndpoint)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(providerSettings.getIssuer()).isEqualTo(issuer);
|
||||||
|
assertThat(providerSettings.getAuthorizationEndpoint()).isEqualTo(authorizationEndpoint);
|
||||||
|
assertThat(providerSettings.getTokenEndpoint()).isEqualTo(tokenEndpoint);
|
||||||
|
assertThat(providerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void settingWhenCustomThenSet() {
|
||||||
|
ProviderSettings providerSettings = ProviderSettings.builder()
|
||||||
|
.setting("name1", "value1")
|
||||||
|
.settings(settings -> settings.put("name2", "value2"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(providerSettings.getSettings()).hasSize(5);
|
||||||
|
assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1");
|
||||||
|
assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void issuerWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ProviderSettings.builder().issuer(null))
|
||||||
|
.withMessage("value cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizationEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ProviderSettings.builder().authorizationEndpoint(null))
|
||||||
|
.withMessage("value cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tokenEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ProviderSettings.builder().tokenEndpoint(null))
|
||||||
|
.withMessage("value cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ProviderSettings.builder().jwkSetEndpoint(null))
|
||||||
|
.withMessage("value cannot be null");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue