mirror of https://github.com/halo-dev/halo
feat: Add bearer token and bearer authentication (#1864)
parent
3f5e83fcae
commit
c999c35a20
|
@ -0,0 +1,92 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Base class for {@link AbstractAuthenticationToken} implementations that expose common
|
||||
* attributes between different OAuth 2.0 Access Token Formats.
|
||||
*
|
||||
* <p>
|
||||
* For example, a {@link Jwt} could expose its {@link Jwt#getClaims() claims} via
|
||||
* {@link #getTokenAttributes()} or an "Introspected" OAuth 2.0 Access Token
|
||||
* could expose the attributes of the Introspection Response via
|
||||
* {@link #getTokenAttributes()}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see OAuth2AccessToken
|
||||
* @see Jwt
|
||||
* @see <a href="https://tools.ietf.org/search/rfc7662#section-2.2">2.2 Introspection Response</a>
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public abstract class AbstractOAuth2TokenAuthenticationToken<T extends AbstractOAuth2Token>
|
||||
extends AbstractAuthenticationToken {
|
||||
|
||||
private Object principal;
|
||||
|
||||
private Object credentials;
|
||||
|
||||
private T token;
|
||||
|
||||
/**
|
||||
* Sub-class constructor.
|
||||
*/
|
||||
protected AbstractOAuth2TokenAuthenticationToken(T token) {
|
||||
|
||||
this(token, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-class constructor.
|
||||
*
|
||||
* @param authorities the authorities assigned to the Access Token
|
||||
*/
|
||||
protected AbstractOAuth2TokenAuthenticationToken(T token,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
|
||||
this(token, token, token, authorities);
|
||||
}
|
||||
|
||||
protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Object credentials,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
|
||||
super(authorities);
|
||||
Assert.notNull(token, "token cannot be null");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
this.principal = principal;
|
||||
this.credentials = credentials;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token bound to this {@link Authentication}.
|
||||
*/
|
||||
public final T getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attributes of the access token.
|
||||
*
|
||||
* @return a {@code Map} of the attributes in the access token.
|
||||
*/
|
||||
public abstract Map<String, Object> getTokenAttributes();
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.Transient;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} token that represents a
|
||||
* successful authentication as obtained through a bearer token.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Transient
|
||||
public class BearerTokenAuthentication
|
||||
extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken> {
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
/**
|
||||
* Constructs a {@link BearerTokenAuthentication} with the provided arguments
|
||||
*
|
||||
* @param principal The OAuth 2.0 attributes
|
||||
* @param credentials The verified token
|
||||
* @param authorities The authorities associated with the given token
|
||||
*/
|
||||
public BearerTokenAuthentication(OAuth2AuthenticatedPrincipal principal,
|
||||
OAuth2AccessToken credentials,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(credentials, principal, credentials, authorities);
|
||||
Assert.isTrue(credentials.getTokenType() == OAuth2AccessToken.TokenType.BEARER,
|
||||
"credentials must be a bearer token");
|
||||
this.attributes =
|
||||
Collections.unmodifiableMap(new LinkedHashMap<>(principal.getAttributes()));
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTokenAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} that contains a
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private final String token;
|
||||
|
||||
/**
|
||||
* Create a {@code BearerTokenAuthenticationToken} using the provided parameter(s)
|
||||
*
|
||||
* @param token - the bearer token
|
||||
*/
|
||||
public BearerTokenAuthenticationToken(String token) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(token, "token cannot be empty");
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
|
||||
*
|
||||
* @return the token that proves the caller's authority to perform the
|
||||
* {@link jakarta.servlet.http.HttpServletRequest}
|
||||
*/
|
||||
public String getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.getToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.getToken();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package run.halo.app.authentication.verifyer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
import com.nimbusds.jose.shaded.json.JSONObject;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthentication;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenAuthentication}
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class BearerTokenAuthenticationTest {
|
||||
private final OAuth2AccessToken
|
||||
token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token",
|
||||
Instant.now(), Instant.now().plusSeconds(3600));
|
||||
|
||||
private final String name = "sub";
|
||||
|
||||
private final Map<String, Object> attributesMap = new HashMap<>();
|
||||
|
||||
private DefaultOAuth2AuthenticatedPrincipal principal;
|
||||
|
||||
private final Collection<GrantedAuthority> authorities =
|
||||
AuthorityUtils.createAuthorityList("USER");
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.attributesMap.put(OAuth2TokenIntrospectionClaimNames.SUB, this.name);
|
||||
this.attributesMap.put(OAuth2TokenIntrospectionClaimNames.USERNAME, "username");
|
||||
this.principal = new DefaultOAuth2AuthenticatedPrincipal(this.attributesMap, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNameWhenConfiguredInConstructorThenReturnsName() {
|
||||
OAuth2AuthenticatedPrincipal principal =
|
||||
new DefaultOAuth2AuthenticatedPrincipal(this.name, this.attributesMap,
|
||||
this.authorities);
|
||||
BearerTokenAuthentication authenticated =
|
||||
new BearerTokenAuthentication(principal, this.token,
|
||||
this.authorities);
|
||||
assertThat(authenticated.getName()).isEqualTo(this.name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNameWhenHasNoSubjectThenReturnsNull() {
|
||||
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(
|
||||
Collections.singletonMap("claim", "value"), null);
|
||||
BearerTokenAuthentication authenticated =
|
||||
new BearerTokenAuthentication(principal, this.token, null);
|
||||
assertThat(authenticated.getName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNameWhenTokenHasUsernameThenReturnsUsernameAttribute() {
|
||||
BearerTokenAuthentication authenticated =
|
||||
new BearerTokenAuthentication(this.principal, this.token, null);
|
||||
assertThat(authenticated.getName())
|
||||
.isEqualTo(this.principal.getAttribute(OAuth2TokenIntrospectionClaimNames.SUB));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenTokenIsNullThenThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new BearerTokenAuthentication(this.principal, null, null))
|
||||
.withMessageContaining("token cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenCredentialIsNullThenThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new BearerTokenAuthentication(null, this.token, null))
|
||||
.withMessageContaining("principal cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenPassingAllAttributesThenTokenIsAuthenticated() {
|
||||
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal("harris",
|
||||
Collections.singletonMap("claim", "value"), null);
|
||||
BearerTokenAuthentication authenticated =
|
||||
new BearerTokenAuthentication(principal, this.token, null);
|
||||
assertThat(authenticated.isAuthenticated()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTokenAttributesWhenHasTokenThenReturnsThem() {
|
||||
BearerTokenAuthentication authenticated =
|
||||
new BearerTokenAuthentication(this.principal, this.token,
|
||||
Collections.emptyList());
|
||||
assertThat(authenticated.getTokenAttributes()).isEqualTo(this.principal.getAttributes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthoritiesWhenHasAuthoritiesThenReturnsThem() {
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("USER");
|
||||
BearerTokenAuthentication authenticated =
|
||||
new BearerTokenAuthentication(this.principal, this.token,
|
||||
authorities);
|
||||
assertThat(authenticated.getAuthorities()).isEqualTo(authorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenDefaultParametersThenSetsPrincipalToAttributesCopy() {
|
||||
JSONObject attributes = new JSONObject();
|
||||
attributes.put("active", true);
|
||||
OAuth2AuthenticatedPrincipal
|
||||
principal = new DefaultOAuth2AuthenticatedPrincipal(attributes, null);
|
||||
BearerTokenAuthentication token =
|
||||
new BearerTokenAuthentication(principal, this.token, null);
|
||||
assertThat(token.getPrincipal()).isNotSameAs(attributes);
|
||||
assertThat(token.getTokenAttributes()).isNotSameAs(attributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringWhenAttributesContainsURLThenDoesNotFail() throws Exception {
|
||||
JSONObject attributes =
|
||||
new JSONObject(Collections.singletonMap("iss", new URL("https://idp.example.com")));
|
||||
OAuth2AuthenticatedPrincipal principal =
|
||||
new DefaultOAuth2AuthenticatedPrincipal(attributes, null);
|
||||
BearerTokenAuthentication token =
|
||||
new BearerTokenAuthentication(principal, this.token, null);
|
||||
assertThat(token.toString()).isNotNull();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package run.halo.app.authentication.verifyer;
|
||||
|
||||
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.verifier.BearerTokenAuthenticationToken;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenAuthenticationToken}
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class BearerTokenAuthenticationTokenTest {
|
||||
@Test
|
||||
public void constructorWhenTokenIsNullThenThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new BearerTokenAuthenticationToken(null))
|
||||
.withMessageContaining("token cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenTokenIsEmptyThenThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new BearerTokenAuthenticationToken(""))
|
||||
.withMessageContaining("token cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenTokenHasValueThenConstructedCorrectly() {
|
||||
BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token");
|
||||
assertThat(token.getToken()).isEqualTo("token");
|
||||
assertThat(token.getPrincipal()).isEqualTo("token");
|
||||
assertThat(token.getCredentials()).isEqualTo("token");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue