mirror of https://github.com/halo-dev/halo
feat: Add InMemoryOAuth2AuthorizationService for development or testing (#1854)
parent
eee58989fe
commit
ad562b4917
|
@ -30,10 +30,10 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||||
|
import run.halo.app.identity.authentication.InMemoryOAuth2AuthorizationService;
|
||||||
import run.halo.app.identity.authentication.JwtDaoAuthenticationProvider;
|
import run.halo.app.identity.authentication.JwtDaoAuthenticationProvider;
|
||||||
import run.halo.app.identity.authentication.JwtGenerator;
|
import run.halo.app.identity.authentication.JwtGenerator;
|
||||||
import run.halo.app.identity.authentication.JwtUsernamePasswordAuthenticationFilter;
|
import run.halo.app.identity.authentication.JwtUsernamePasswordAuthenticationFilter;
|
||||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
|
||||||
import run.halo.app.identity.authentication.ProviderContextFilter;
|
import run.halo.app.identity.authentication.ProviderContextFilter;
|
||||||
import run.halo.app.identity.authentication.ProviderSettings;
|
import run.halo.app.identity.authentication.ProviderSettings;
|
||||||
import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler;
|
import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler;
|
||||||
|
@ -110,7 +110,8 @@ public class WebSecurityConfig {
|
||||||
@Bean
|
@Bean
|
||||||
JwtDaoAuthenticationProvider jwtDaoAuthenticationProvider() {
|
JwtDaoAuthenticationProvider jwtDaoAuthenticationProvider() {
|
||||||
JwtDaoAuthenticationProvider authenticationProvider =
|
JwtDaoAuthenticationProvider authenticationProvider =
|
||||||
new JwtDaoAuthenticationProvider(jwtGenerator(), new OAuth2AuthorizationService());
|
new JwtDaoAuthenticationProvider(jwtGenerator(),
|
||||||
|
new InMemoryOAuth2AuthorizationService());
|
||||||
authenticationProvider.setUserDetailsService(userDetailsService());
|
authenticationProvider.setUserDetailsService(userDetailsService());
|
||||||
authenticationProvider.setPasswordEncoder(passwordEncoder());
|
authenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||||
return authenticationProvider;
|
return authenticationProvider;
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link OAuth2AuthorizationService} that stores {@link OAuth2Authorization}'s in-memory.<p>
|
||||||
|
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @see OAuth2AuthorizationService
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class InMemoryOAuth2AuthorizationService implements OAuth2AuthorizationService {
|
||||||
|
private int maxInitializedAuthorizations = 100;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stores "initialized" (uncompleted) authorizations, where an access token has not yet been
|
||||||
|
* granted.
|
||||||
|
* This state occurs with the authorization_code grant flow during the user consent step OR
|
||||||
|
* when the code is returned in the authorization response but the access token request is
|
||||||
|
* not yet initiated.
|
||||||
|
*/
|
||||||
|
private Map<String, OAuth2Authorization> initializedAuthorizations =
|
||||||
|
Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stores "completed" authorizations, where an access token has been granted.
|
||||||
|
*/
|
||||||
|
private final Map<String, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Constructor used for testing only.
|
||||||
|
*/
|
||||||
|
public InMemoryOAuth2AuthorizationService(int maxInitializedAuthorizations) {
|
||||||
|
this.maxInitializedAuthorizations = maxInitializedAuthorizations;
|
||||||
|
this.initializedAuthorizations =
|
||||||
|
Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code InMemoryOAuth2AuthorizationService}.
|
||||||
|
*/
|
||||||
|
public InMemoryOAuth2AuthorizationService() {
|
||||||
|
this(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param authorizations the authorization(s)
|
||||||
|
*/
|
||||||
|
public InMemoryOAuth2AuthorizationService(OAuth2Authorization... authorizations) {
|
||||||
|
this(Arrays.asList(authorizations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param authorizations the authorization(s)
|
||||||
|
*/
|
||||||
|
public InMemoryOAuth2AuthorizationService(List<OAuth2Authorization> authorizations) {
|
||||||
|
Assert.notNull(authorizations, "authorizations cannot be null");
|
||||||
|
authorizations.forEach(authorization -> {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
Assert.isTrue(!this.authorizations.containsKey(authorization.getId()),
|
||||||
|
"The authorization must be unique. Found duplicate identifier: "
|
||||||
|
+ authorization.getId());
|
||||||
|
this.authorizations.put(authorization.getId(), authorization);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(OAuth2Authorization authorization) {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
if (isComplete(authorization)) {
|
||||||
|
this.authorizations.put(authorization.getId(), authorization);
|
||||||
|
} else {
|
||||||
|
this.initializedAuthorizations.put(authorization.getId(), authorization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(OAuth2Authorization authorization) {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
if (isComplete(authorization)) {
|
||||||
|
this.authorizations.remove(authorization.getId(), authorization);
|
||||||
|
} else {
|
||||||
|
this.initializedAuthorizations.remove(authorization.getId(), authorization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public OAuth2Authorization findById(String id) {
|
||||||
|
Assert.hasText(id, "id cannot be empty");
|
||||||
|
OAuth2Authorization authorization = this.authorizations.get(id);
|
||||||
|
return authorization != null
|
||||||
|
? authorization :
|
||||||
|
this.initializedAuthorizations.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
|
||||||
|
Assert.hasText(token, "token cannot be empty");
|
||||||
|
for (OAuth2Authorization authorization : this.authorizations.values()) {
|
||||||
|
if (hasToken(authorization, token, tokenType)) {
|
||||||
|
return authorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (OAuth2Authorization authorization : this.initializedAuthorizations.values()) {
|
||||||
|
if (hasToken(authorization, token, tokenType)) {
|
||||||
|
return authorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isComplete(OAuth2Authorization authorization) {
|
||||||
|
return authorization.getAccessToken() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasToken(OAuth2Authorization authorization, String token,
|
||||||
|
@Nullable OAuth2TokenType tokenType) {
|
||||||
|
if (tokenType == null) {
|
||||||
|
return matchesAccessToken(authorization, token)
|
||||||
|
|| matchesRefreshToken(authorization, token);
|
||||||
|
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
|
||||||
|
return matchesAccessToken(authorization, token);
|
||||||
|
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
|
||||||
|
return matchesRefreshToken(authorization, token);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesAccessToken(OAuth2Authorization authorization, String token) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
|
||||||
|
authorization.getToken(OAuth2AccessToken.class);
|
||||||
|
return accessToken != null && accessToken.getToken().getTokenValue().equals(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesRefreshToken(OAuth2Authorization authorization, String token) {
|
||||||
|
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
|
||||||
|
authorization.getToken(OAuth2RefreshToken.class);
|
||||||
|
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
|
||||||
|
private final int maxSize;
|
||||||
|
|
||||||
|
private MaxSizeHashMap(int maxSize) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||||
|
return size() > this.maxSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
@ -21,7 +22,6 @@ import org.springframework.security.oauth2.core.OAuth2Token;
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
|
public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
private static final OAuth2TokenType PASSWORD_TOKEN = new OAuth2TokenType("password");
|
|
||||||
private static final String ERROR_URI =
|
private static final String ERROR_URI =
|
||||||
"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;
|
||||||
|
@ -41,18 +41,6 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
(UsernamePasswordAuthenticationToken) super.createSuccessAuthentication(principal,
|
(UsernamePasswordAuthenticationToken) super.createSuccessAuthentication(principal,
|
||||||
authentication, user);
|
authentication, user);
|
||||||
|
|
||||||
OAuth2Authorization authorization = this.authorizationService.findByUsername(
|
|
||||||
usernamePasswordAuthenticationToken.getName(), PASSWORD_TOKEN);
|
|
||||||
|
|
||||||
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
|
|
||||||
authorization.getRefreshToken();
|
|
||||||
if (refreshToken == null || !refreshToken.isActive()) {
|
|
||||||
// As per https://tools.ietf.org/html/rfc6749#section-5.2
|
|
||||||
// invalid_grant: The provided authorization grant (e.g., authorization code,
|
|
||||||
// resource owner credentials) or refresh token is invalid, expired, revoked [...].
|
|
||||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> scopes = usernamePasswordAuthenticationToken.getAuthorities().stream()
|
Set<String> scopes = usernamePasswordAuthenticationToken.getAuthorities().stream()
|
||||||
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
|
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
@ -61,7 +49,10 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
.providerContext(ProviderContextHolder.getProviderContext())
|
.providerContext(ProviderContextHolder.getProviderContext())
|
||||||
.authorizedScopes(scopes);
|
.authorizedScopes(scopes);
|
||||||
|
|
||||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
OAuth2Authorization.Builder authorizationBuilder = new OAuth2Authorization.Builder()
|
||||||
|
.principalName(authentication.getName())
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||||
|
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, scopes);
|
||||||
|
|
||||||
// ----- Access token -----
|
// ----- Access token -----
|
||||||
OAuth2TokenContext tokenContext =
|
OAuth2TokenContext tokenContext =
|
||||||
|
@ -89,7 +80,7 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
|
||||||
ProviderContextHolder.getProviderContext().providerSettings();
|
ProviderContextHolder.getProviderContext().providerSettings();
|
||||||
|
|
||||||
// ----- Refresh token -----
|
// ----- Refresh token -----
|
||||||
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
|
OAuth2RefreshToken currentRefreshToken = null;
|
||||||
if (!providerSettings.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);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||||
|
@ -31,6 +32,7 @@ public class OAuth2Authorization implements Serializable {
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
private String principalName;
|
private String principalName;
|
||||||
|
private AuthorizationGrantType authorizationGrantType;
|
||||||
private Map<Class<? extends OAuth2Token>, Token<?>> tokens;
|
private Map<Class<? extends OAuth2Token>, Token<?>> tokens;
|
||||||
private Map<String, Object> attributes;
|
private Map<String, Object> attributes;
|
||||||
|
|
||||||
|
@ -55,6 +57,16 @@ public class OAuth2Authorization implements Serializable {
|
||||||
return this.principalName;
|
return this.principalName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link AuthorizationGrantType authorization grant type} used for the
|
||||||
|
* authorization.
|
||||||
|
*
|
||||||
|
* @return the {@link AuthorizationGrantType} used for the authorization
|
||||||
|
*/
|
||||||
|
public AuthorizationGrantType getAuthorizationGrantType() {
|
||||||
|
return this.authorizationGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link Token} of type {@link OAuth2AccessToken}.
|
* Returns the {@link Token} of type {@link OAuth2AccessToken}.
|
||||||
*
|
*
|
||||||
|
@ -319,6 +331,7 @@ public class OAuth2Authorization implements Serializable {
|
||||||
public static class Builder implements Serializable {
|
public static class Builder implements Serializable {
|
||||||
private String id;
|
private String id;
|
||||||
private String principalName;
|
private String principalName;
|
||||||
|
private AuthorizationGrantType authorizationGrantType;
|
||||||
private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();
|
private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();
|
||||||
private final Map<String, Object> attributes = new HashMap<>();
|
private final Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
@ -344,6 +357,18 @@ public class OAuth2Authorization implements Serializable {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link AuthorizationGrantType authorization grant type} used for the
|
||||||
|
* authorization.
|
||||||
|
*
|
||||||
|
* @param authorizationGrantType the {@link AuthorizationGrantType}
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
|
||||||
|
this.authorizationGrantType = authorizationGrantType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link OAuth2AccessToken access token}.
|
* Sets the {@link OAuth2AccessToken access token}.
|
||||||
*
|
*
|
||||||
|
@ -444,6 +469,7 @@ public class OAuth2Authorization implements Serializable {
|
||||||
}
|
}
|
||||||
authorization.id = this.id;
|
authorization.id = this.id;
|
||||||
authorization.principalName = this.principalName;
|
authorization.principalName = this.principalName;
|
||||||
|
authorization.authorizationGrantType = this.authorizationGrantType;
|
||||||
authorization.tokens = Collections.unmodifiableMap(this.tokens);
|
authorization.tokens = Collections.unmodifiableMap(this.tokens);
|
||||||
authorization.attributes = Collections.unmodifiableMap(this.attributes);
|
authorization.attributes = Collections.unmodifiableMap(this.attributes);
|
||||||
return authorization;
|
return authorization;
|
||||||
|
|
|
@ -1,27 +1,45 @@
|
||||||
package run.halo.app.identity.authentication;
|
package run.halo.app.identity.authentication;
|
||||||
|
|
||||||
import java.time.Instant;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
|
||||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class OAuth2AuthorizationService {
|
public interface OAuth2AuthorizationService {
|
||||||
OAuth2Authorization findByUsername(String username, OAuth2TokenType oauth2TokenType) {
|
/**
|
||||||
// TODO to be implementation
|
* Returns the {@link OAuth2Authorization} containing the provided {@code token},
|
||||||
return new OAuth2Authorization.Builder().id("id")
|
* or {@code null} if not found.
|
||||||
.accessToken(new OAuth2AccessToken(
|
*
|
||||||
OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(),
|
* @param token the token credential
|
||||||
Instant.now().plusMillis(123)))
|
* @param tokenType the {@link OAuth2TokenType token type}
|
||||||
.refreshToken(
|
* @return the {@link OAuth2Authorization} if found, otherwise {@code null}
|
||||||
new OAuth2RefreshToken("refresh_token", Instant.now()))
|
*/
|
||||||
.principalName("guqing")
|
@Nullable
|
||||||
.build();
|
OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType);
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Saves the {@link OAuth2Authorization}.
|
||||||
|
*
|
||||||
|
* @param authorization the {@link OAuth2Authorization}
|
||||||
|
*/
|
||||||
|
void save(OAuth2Authorization authorization);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the {@link OAuth2Authorization}.
|
||||||
|
*
|
||||||
|
* @param authorization the {@link OAuth2Authorization}
|
||||||
|
*/
|
||||||
|
void remove(OAuth2Authorization authorization);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link OAuth2Authorization} identified by the provided {@code id},
|
||||||
|
* or {@code null} if not found.
|
||||||
|
*
|
||||||
|
* @param id the authorization identifier
|
||||||
|
* @return the {@link OAuth2Authorization} if found, otherwise {@code null}
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
OAuth2Authorization findById(String id);
|
||||||
|
|
||||||
void save(OAuth2Authorization authorization) {
|
|
||||||
// TODO to be implementation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
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.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import run.halo.app.identity.authentication.InMemoryOAuth2AuthorizationService;
|
||||||
|
import run.halo.app.identity.authentication.OAuth2Authorization;
|
||||||
|
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||||
|
import run.halo.app.identity.authentication.OAuth2TokenType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link OAuth2AuthorizationService} that stores {@link OAuth2Authorization}'s in-memory.<p>
|
||||||
|
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @see OAuth2AuthorizationService
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class InMemoryOAuth2AuthorizationServiceTest {
|
||||||
|
private static final String ID = "id";
|
||||||
|
private static final String PRINCIPAL_NAME = "principal";
|
||||||
|
private static final AuthorizationGrantType AUTHORIZATION_GRANT_TYPE =
|
||||||
|
AuthorizationGrantType.PASSWORD;
|
||||||
|
private static final OAuth2AccessToken TOKEN =
|
||||||
|
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access_token",
|
||||||
|
Instant.now(), Instant.now().plusSeconds(256));
|
||||||
|
private InMemoryOAuth2AuthorizationService authorizationService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
this.authorizationService = new InMemoryOAuth2AuthorizationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorVarargsWhenAuthorizationNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> new InMemoryOAuth2AuthorizationService((OAuth2Authorization) null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("authorization cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorListWhenAuthorizationsNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(
|
||||||
|
() -> new InMemoryOAuth2AuthorizationService((List<OAuth2Authorization>) null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("authorizations cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorWhenDuplicateAuthorizationsThenThrowIllegalArgumentException() {
|
||||||
|
OAuth2Authorization authorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.token(TOKEN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThatThrownBy(
|
||||||
|
() -> new InMemoryOAuth2AuthorizationService(authorization, authorization))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The authorization must be unique. Found duplicate identifier: id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void saveWhenAuthorizationNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizationService.save(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("authorization cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void saveWhenAuthorizationNewThenSaved() {
|
||||||
|
OAuth2Authorization expectedAuthorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.token(TOKEN)
|
||||||
|
.build();
|
||||||
|
this.authorizationService.save(expectedAuthorization);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||||
|
TOKEN.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
assertThat(authorization).isEqualTo(expectedAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void saveWhenAuthorizationExistsThenUpdated() {
|
||||||
|
OAuth2Authorization originalAuthorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.token(TOKEN)
|
||||||
|
.build();
|
||||||
|
this.authorizationService.save(originalAuthorization);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = this.authorizationService.findById(
|
||||||
|
originalAuthorization.getId());
|
||||||
|
assertThat(authorization).isEqualTo(originalAuthorization);
|
||||||
|
|
||||||
|
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
||||||
|
.attribute("custom-name-1", "custom-value-1")
|
||||||
|
.build();
|
||||||
|
this.authorizationService.save(updatedAuthorization);
|
||||||
|
|
||||||
|
authorization = this.authorizationService.findById(
|
||||||
|
updatedAuthorization.getId());
|
||||||
|
assertThat(authorization).isEqualTo(updatedAuthorization);
|
||||||
|
assertThat(authorization).isNotEqualTo(originalAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void saveWhenInitializedAuthorizationsReachMaxThenOldestRemoved() {
|
||||||
|
int maxInitializedAuthorizations = 5;
|
||||||
|
InMemoryOAuth2AuthorizationService authorizationService =
|
||||||
|
new InMemoryOAuth2AuthorizationService(maxInitializedAuthorizations);
|
||||||
|
|
||||||
|
OAuth2Authorization initialAuthorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID + "-initial")
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.attribute(OAuth2ParameterNames.STATE, "state-initial")
|
||||||
|
.build();
|
||||||
|
authorizationService.save(initialAuthorization);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization =
|
||||||
|
authorizationService.findById(initialAuthorization.getId());
|
||||||
|
assertThat(authorization).isEqualTo(initialAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removeWhenAuthorizationNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizationService.remove(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("authorization cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removeWhenAuthorizationProvidedThenRemoved() {
|
||||||
|
OAuth2Authorization expectedAuthorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.token(TOKEN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.authorizationService.save(expectedAuthorization);
|
||||||
|
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||||
|
TOKEN.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
assertThat(authorization).isEqualTo(expectedAuthorization);
|
||||||
|
|
||||||
|
this.authorizationService.remove(expectedAuthorization);
|
||||||
|
authorization = this.authorizationService.findByToken(
|
||||||
|
TOKEN.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
assertThat(authorization).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findByIdWhenIdNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizationService.findById(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("id cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findByTokenWhenTokenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(
|
||||||
|
() -> this.authorizationService.findByToken(null, OAuth2TokenType.ACCESS_TOKEN))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("token cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findByTokenWhenAccessTokenExistsThenFound() {
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
"access-token", Instant.now().minusSeconds(60), Instant.now());
|
||||||
|
OAuth2Authorization authorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.token(TOKEN)
|
||||||
|
.accessToken(accessToken)
|
||||||
|
.build();
|
||||||
|
this.authorizationService.save(authorization);
|
||||||
|
|
||||||
|
OAuth2Authorization result = this.authorizationService.findByToken(
|
||||||
|
accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
assertThat(authorization).isEqualTo(result);
|
||||||
|
result = this.authorizationService.findByToken(accessToken.getTokenValue(), null);
|
||||||
|
assertThat(authorization).isEqualTo(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findByTokenWhenRefreshTokenExistsThenFound() {
|
||||||
|
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now());
|
||||||
|
OAuth2Authorization authorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.refreshToken(refreshToken)
|
||||||
|
.build();
|
||||||
|
this.authorizationService.save(authorization);
|
||||||
|
|
||||||
|
OAuth2Authorization result = this.authorizationService.findByToken(
|
||||||
|
refreshToken.getTokenValue(), OAuth2TokenType.REFRESH_TOKEN);
|
||||||
|
assertThat(authorization).isEqualTo(result);
|
||||||
|
result = this.authorizationService.findByToken(refreshToken.getTokenValue(), null);
|
||||||
|
assertThat(authorization).isEqualTo(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findByTokenWhenWrongTokenTypeThenNotFound() {
|
||||||
|
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now());
|
||||||
|
OAuth2Authorization authorization = new OAuth2Authorization.Builder()
|
||||||
|
.id(ID)
|
||||||
|
.principalName(PRINCIPAL_NAME)
|
||||||
|
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
|
||||||
|
.refreshToken(refreshToken)
|
||||||
|
.build();
|
||||||
|
this.authorizationService.save(authorization);
|
||||||
|
|
||||||
|
OAuth2Authorization result = this.authorizationService.findByToken(
|
||||||
|
refreshToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
assertThat(result).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findByTokenWhenTokenDoesNotExistThenNull() {
|
||||||
|
OAuth2Authorization result = this.authorizationService.findByToken(
|
||||||
|
"access-token", OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
assertThat(result).isNull();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue