mirror of https://github.com/halo-dev/halo
feat: Add jwt token and provider for jwt authentication (#1871)
parent
673c2068d9
commit
e8d8ef8f28
|
@ -0,0 +1,40 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationException} that indicates an invalid bearer token.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class InvalidBearerTokenException extends OAuth2AuthenticationException {
|
||||
/**
|
||||
* Construct an instance of {@link InvalidBearerTokenException} given the provided
|
||||
* description.
|
||||
* <p>
|
||||
* The description will be wrapped into an
|
||||
* {@link org.springframework.security.oauth2.core.OAuth2Error} instance as the
|
||||
* {@code error_description}.
|
||||
*
|
||||
* @param description the description
|
||||
*/
|
||||
public InvalidBearerTokenException(String description) {
|
||||
super(BearerTokenErrors.invalidToken(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance of {@link InvalidBearerTokenException} given the provided
|
||||
* description and cause
|
||||
* <p>
|
||||
* The description will be wrapped into an
|
||||
* {@link org.springframework.security.oauth2.core.OAuth2Error} instance as the
|
||||
* {@code error_description}.
|
||||
*
|
||||
* @param description the description
|
||||
* @param cause the causing exception
|
||||
*/
|
||||
public InvalidBearerTokenException(String description, Throwable cause) {
|
||||
super(BearerTokenErrors.invalidToken(description), cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
|
||||
private String principalClaimName = JwtClaimNames.SUB;
|
||||
|
||||
@Override
|
||||
public final AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
|
||||
Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
|
||||
String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
|
||||
return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter Converter<Jwt, Collection<GrantedAuthority>>}
|
||||
* to use. Defaults to {@link JwtGrantedAuthoritiesConverter}.
|
||||
*
|
||||
* @param jwtGrantedAuthoritiesConverter The converter
|
||||
* @see JwtGrantedAuthoritiesConverter
|
||||
* @since 5.2
|
||||
*/
|
||||
public void setJwtGrantedAuthoritiesConverter(
|
||||
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
|
||||
Assert.notNull(jwtGrantedAuthoritiesConverter,
|
||||
"jwtGrantedAuthoritiesConverter cannot be null");
|
||||
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the principal claim name. Defaults to {@link JwtClaimNames#SUB}.
|
||||
*
|
||||
* @param principalClaimName The principal claim name
|
||||
* @since 5.4
|
||||
*/
|
||||
public void setPrincipalClaimName(String principalClaimName) {
|
||||
Assert.hasText(principalClaimName, "principalClaimName cannot be empty");
|
||||
this.principalClaimName = principalClaimName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.BadJwtException;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
|
||||
* s for protecting server resources.
|
||||
* <p>
|
||||
* <p>
|
||||
* This {@link AuthenticationProvider} is responsible for decoding and verifying a
|
||||
* {@link Jwt}-encoded access token, returning its claims set as part of the
|
||||
* {@link Authentication} statement.
|
||||
* <p>
|
||||
* <p>
|
||||
* Scopes are translated into {@link GrantedAuthority}s according to the following
|
||||
* algorithm:
|
||||
* <p>
|
||||
* 1. If there is a "scope" or "scp" attribute, then if a {@link String}, then split by
|
||||
* spaces and return, or if a {@link Collection}, then simply return 2. Take the resulting
|
||||
* {@link Collection} of {@link String}s and prepend the "SCOPE_" keyword, adding as
|
||||
* {@link GrantedAuthority}s.
|
||||
*
|
||||
* @author guqing
|
||||
* @see AuthenticationProvider
|
||||
* @see JwtDecoder
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class JwtAuthenticationProvider implements AuthenticationProvider {
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
|
||||
new JwtAuthenticationConverter();
|
||||
|
||||
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
|
||||
Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
|
||||
this.jwtDecoder = jwtDecoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and validate the
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.
|
||||
*
|
||||
* @param authentication the authentication request object.
|
||||
* @return A successful authentication
|
||||
* @throws AuthenticationException if authentication failed for some reason
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication)
|
||||
throws AuthenticationException {
|
||||
BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
|
||||
Jwt jwt = getJwt(bearer);
|
||||
AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
|
||||
if (token != null) {
|
||||
token.setDetails(bearer.getDetails());
|
||||
}
|
||||
log.debug("Authenticated token");
|
||||
return token;
|
||||
}
|
||||
|
||||
private Jwt getJwt(BearerTokenAuthenticationToken bearer) {
|
||||
try {
|
||||
return this.jwtDecoder.decode(bearer.getToken());
|
||||
} catch (BadJwtException failed) {
|
||||
log.debug("Failed to authenticate since the JWT was invalid");
|
||||
throw new InvalidBearerTokenException(failed.getMessage(), failed);
|
||||
} catch (JwtException failed) {
|
||||
throw new AuthenticationServiceException(failed.getMessage(), failed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
public void setJwtAuthenticationConverter(
|
||||
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) {
|
||||
Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
|
||||
this.jwtAuthenticationConverter = jwtAuthenticationConverter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.Transient;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} representing a
|
||||
* {@link Jwt} {@code Authentication}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see AbstractOAuth2TokenAuthenticationToken
|
||||
* @see Jwt
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Transient
|
||||
public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param jwt the JWT
|
||||
*/
|
||||
public JwtAuthenticationToken(Jwt jwt) {
|
||||
super(jwt);
|
||||
this.name = jwt.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param jwt the JWT
|
||||
* @param authorities the authorities assigned to the JWT
|
||||
*/
|
||||
public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(jwt, authorities);
|
||||
this.setAuthenticated(true);
|
||||
this.name = jwt.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param jwt the JWT
|
||||
* @param authorities the authorities assigned to the JWT
|
||||
* @param name the principal name
|
||||
*/
|
||||
public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities,
|
||||
String name) {
|
||||
super(jwt, authorities);
|
||||
this.setAuthenticated(true);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTokenAttributes() {
|
||||
return this.getToken().getClaims();
|
||||
}
|
||||
|
||||
/**
|
||||
* The principal name which is, by default, the {@link Jwt}'s subject
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Extracts the {@link GrantedAuthority}s from scope attributes typically found in a
|
||||
* {@link Jwt}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class JwtGrantedAuthoritiesConverter implements
|
||||
Converter<Jwt, Collection<GrantedAuthority>> {
|
||||
private static final String DEFAULT_AUTHORITY_PREFIX = "SCOPE_";
|
||||
|
||||
private static final Collection<String> WELL_KNOWN_AUTHORITIES_CLAIM_NAMES =
|
||||
List.of("scope", "scp");
|
||||
|
||||
private String authorityPrefix = DEFAULT_AUTHORITY_PREFIX;
|
||||
|
||||
private String authoritiesClaimName;
|
||||
|
||||
/**
|
||||
* Extract {@link GrantedAuthority}s from the given {@link Jwt}.
|
||||
*
|
||||
* @param jwt The {@link Jwt} token
|
||||
* @return The {@link GrantedAuthority authorities} read from the token scopes
|
||||
*/
|
||||
@Override
|
||||
public Collection<GrantedAuthority> convert(@NonNull Jwt jwt) {
|
||||
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
||||
for (String authority : getAuthorities(jwt)) {
|
||||
grantedAuthorities.add(new SimpleGrantedAuthority(this.authorityPrefix + authority));
|
||||
}
|
||||
return grantedAuthorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the prefix to use for {@link GrantedAuthority authorities} mapped by this
|
||||
* converter. Defaults to
|
||||
* {@link JwtGrantedAuthoritiesConverter#DEFAULT_AUTHORITY_PREFIX}.
|
||||
*
|
||||
* @param authorityPrefix The authority prefix
|
||||
* @since 5.2
|
||||
*/
|
||||
public void setAuthorityPrefix(String authorityPrefix) {
|
||||
Assert.notNull(authorityPrefix, "authorityPrefix cannot be null");
|
||||
this.authorityPrefix = authorityPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of token claim to use for mapping {@link GrantedAuthority
|
||||
* authorities} by this converter. Defaults to
|
||||
* {@link JwtGrantedAuthoritiesConverter#WELL_KNOWN_AUTHORITIES_CLAIM_NAMES}.
|
||||
*
|
||||
* @param authoritiesClaimName The token claim name to map authorities
|
||||
* @since 5.2
|
||||
*/
|
||||
public void setAuthoritiesClaimName(String authoritiesClaimName) {
|
||||
Assert.hasText(authoritiesClaimName, "authoritiesClaimName cannot be empty");
|
||||
this.authoritiesClaimName = authoritiesClaimName;
|
||||
}
|
||||
|
||||
private String getAuthoritiesClaimName(Jwt jwt) {
|
||||
if (this.authoritiesClaimName != null) {
|
||||
return this.authoritiesClaimName;
|
||||
}
|
||||
for (String claimName : WELL_KNOWN_AUTHORITIES_CLAIM_NAMES) {
|
||||
if (jwt.hasClaim(claimName)) {
|
||||
return claimName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Collection<String> getAuthorities(Jwt jwt) {
|
||||
String claimName = getAuthoritiesClaimName(jwt);
|
||||
if (claimName == null) {
|
||||
log.trace(
|
||||
"Returning no authorities since could not find any claims that might contain "
|
||||
+ "scopes");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
log.trace("Looking for scopes in claim [{}]", claimName);
|
||||
Object authorities = jwt.getClaim(claimName);
|
||||
if (authorities instanceof String) {
|
||||
if (StringUtils.hasText((String) authorities)) {
|
||||
return Arrays.asList(((String) authorities).split(" "));
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (authorities instanceof Collection) {
|
||||
return castAuthoritiesToCollection(authorities);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Collection<String> castAuthoritiesToCollection(Object authorities) {
|
||||
return (Collection<String>) authorities;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package run.halo.app.authentication.verifyer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.jwt.BadJwtException;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationToken;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenErrorCodes;
|
||||
import run.halo.app.identity.authentication.verifier.JwtAuthenticationProvider;
|
||||
import run.halo.app.identity.authentication.verifier.JwtAuthenticationToken;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtAuthenticationProvider}
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class JwtAuthenticationProviderTest {
|
||||
@Mock
|
||||
Converter<Jwt, JwtAuthenticationToken> jwtAuthenticationConverter;
|
||||
|
||||
@Mock
|
||||
JwtDecoder jwtDecoder;
|
||||
|
||||
JwtAuthenticationProvider provider;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.provider = new JwtAuthenticationProvider(this.jwtDecoder);
|
||||
this.provider.setJwtAuthenticationConverter(this.jwtAuthenticationConverter);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("authenticate when jwt decodes then authentication has attributes contained in "
|
||||
+ "Jwt")
|
||||
public void authenticateThenAttributesContainedInJwt() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
Jwt jwt = TestJwts.jwt().claim("name", "value").build();
|
||||
given(this.jwtDecoder.decode("token")).willReturn(jwt);
|
||||
given(this.jwtAuthenticationConverter.convert(jwt)).willReturn(
|
||||
new JwtAuthenticationToken(jwt));
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
assertThat(authentication.getTokenAttributes()).containsEntry("name", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenJwtDecodeFailsThenRespondsWithInvalidToken() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
given(this.jwtDecoder.decode("token")).willThrow(BadJwtException.class);
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.matches(errorCode(BearerTokenErrorCodes.INVALID_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("authenticate when decoder throws incompatible error message then wraps with "
|
||||
+ "generic one")
|
||||
public void authenticateWrapsWithGenericOne() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
given(this.jwtDecoder.decode(token.getToken())).willThrow(
|
||||
new BadJwtException("with \"invalid\" chars"));
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.satisfies((ex) -> assertThat(ex).hasFieldOrPropertyWithValue("error.description",
|
||||
"Invalid token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenDecoderFailsGenericallyThenThrowsGenericException() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
given(this.jwtDecoder.decode(token.getToken()))
|
||||
.willThrow(new JwtException("no jwk set"));
|
||||
assertThatExceptionOfType(AuthenticationException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(token))
|
||||
.isNotInstanceOf(OAuth2AuthenticationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("authenticate when converter returns authentication then provider propagates it")
|
||||
public void authenticateProviderPropagatesIt() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
Object details = mock(Object.class);
|
||||
token.setDetails(details);
|
||||
Jwt jwt = TestJwts.jwt().build();
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
|
||||
given(this.jwtDecoder.decode(token.getToken())).willReturn(jwt);
|
||||
given(this.jwtAuthenticationConverter.convert(jwt)).willReturn(authentication);
|
||||
assertThat(this.provider.authenticate(token))
|
||||
.isEqualTo(authentication).hasFieldOrPropertyWithValue("details",
|
||||
details);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenBearerTokenAuthenticationTokenThenReturnsTrue() {
|
||||
assertThat(this.provider.supports(BearerTokenAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("unSupports when BearerTokenAuthenticationToken then returns true")
|
||||
public void unSupportsJwtAuthenticationToken() {
|
||||
assertThat(this.provider.supports(JwtAuthenticationToken.class)).isFalse();
|
||||
}
|
||||
|
||||
private BearerTokenAuthenticationToken authentication() {
|
||||
return new BearerTokenAuthenticationToken("token");
|
||||
}
|
||||
|
||||
private Predicate<? super Throwable> errorCode(String errorCode) {
|
||||
return (failed) -> Objects.equals(
|
||||
((OAuth2AuthenticationException) failed).getError().getErrorCode(), errorCode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package run.halo.app.authentication.verifyer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import run.halo.app.identity.authentication.verifier.JwtGrantedAuthoritiesConverter;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtGrantedAuthoritiesConverter}
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JwtGrantedAuthoritiesConverterTest {
|
||||
|
||||
@Test
|
||||
public void setAuthorityPrefixWithNullThenException() {
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> jwtGrantedAuthoritiesConverter.setAuthorityPrefix(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("scope", "message:read message:write")
|
||||
.build();
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("SCOPE_message:read"),
|
||||
new SimpleGrantedAuthority("SCOPE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convert with custom authority prefix when token has scope attribute then "
|
||||
+ "translated to authorities")
|
||||
public void convertWithCustomAuthorityPrefixThenTranslatedToAuthorities() {
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("scope", "message:read message:write")
|
||||
.build();
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("ROLE_message:read"),
|
||||
new SimpleGrantedAuthority("ROLE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convert with blank as custom authority prefix when token has scope attribute "
|
||||
+ "then translated to authorities")
|
||||
public void convertWithBlankAsCustomAuthorityPrefixThenTranslatedToAuthorities() {
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("scope", "message:read message:write")
|
||||
.build();
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("message:read"),
|
||||
new SimpleGrantedAuthority("message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() {
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("scope", "")
|
||||
.build();
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
assertThat(authorities).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("scp", List.of("message:read", "message:write"))
|
||||
.build();
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("SCOPE_message:read"),
|
||||
new SimpleGrantedAuthority("SCOPE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convert when token has both scope and scp then scope"
|
||||
+ " attribute is translated to authorities")
|
||||
public void convertWhenTokenHasBothScopeAndScp() {
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("scp", List.of("message:read", "message:write"))
|
||||
.claim("scope", "missive:read missive:write")
|
||||
.build();
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
|
||||
new JwtGrantedAuthoritiesConverter();
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("SCOPE_missive:read"),
|
||||
new SimpleGrantedAuthority("SCOPE_missive:write"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package run.halo.app.authentication.verifyer;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class TestJwts {
|
||||
|
||||
private TestJwts() {
|
||||
}
|
||||
|
||||
public static Jwt.Builder jwt() {
|
||||
return Jwt.withTokenValue("token")
|
||||
.header("alg", "none")
|
||||
.audience(List.of("https://audience.example.org"))
|
||||
.expiresAt(Instant.MAX)
|
||||
.issuedAt(Instant.MIN)
|
||||
.issuer("https://issuer.example.org")
|
||||
.jti("jti")
|
||||
.notBefore(Instant.MIN)
|
||||
.subject("mock-test-subject");
|
||||
}
|
||||
|
||||
public static Jwt user() {
|
||||
return jwt()
|
||||
.claim("sub", "mock-test-subject")
|
||||
.build();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue