Resource Server Jwt Support
Introducing initial support for Jwt-Encoded Bearer Token authorization with remote JWK set signature verification. High-level features include: - Accepting bearer tokens as headers and form or query parameters - Verifying signatures from a remote Jwk set And: - A DSL for easy configuration - A sample to demonstrate usage Fixes: gh-5128 Fixes: gh-5125 Fixes: gh-5121 Fixes: gh-5130 Fixes: gh-5226 Fixes: gh-5237
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} that contains a
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>.
|
||||
*
|
||||
* Used by {@link BearerTokenAuthenticationFilter} to prepare an authentication attempt and supported
|
||||
* by {@link JwtAuthenticationProvider}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.1
|
||||
*/
|
||||
public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
private 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" target="_blank">Bearer Token</a>
|
||||
* @return the token that proves the caller's authority to perform the {@link javax.servlet.http.HttpServletRequest}
|
||||
*/
|
||||
public String getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.getToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A representation of a <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">Bearer Token Error</a>.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Josh Cummings
|
||||
* @since 5.1
|
||||
* @see BearerTokenErrorCodes
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate
|
||||
* Response Header Field</a>
|
||||
*/
|
||||
public final class BearerTokenError extends OAuth2Error {
|
||||
|
||||
private final HttpStatus httpStatus;
|
||||
|
||||
private final String scope;
|
||||
|
||||
/**
|
||||
* Create a {@code BearerTokenError} using the provided parameters
|
||||
*
|
||||
* @param errorCode the error code
|
||||
* @param httpStatus the HTTP status
|
||||
*/
|
||||
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri) {
|
||||
this(errorCode, httpStatus, description, errorUri, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code BearerTokenError} using the provided parameters
|
||||
*
|
||||
* @param errorCode the error code
|
||||
* @param httpStatus the HTTP status
|
||||
* @param description the description
|
||||
* @param errorUri the URI
|
||||
* @param scope the scope
|
||||
*/
|
||||
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri, String scope) {
|
||||
super(errorCode, description, errorUri);
|
||||
Assert.notNull(httpStatus, "httpStatus cannot be null");
|
||||
|
||||
Assert.isTrue(isDescriptionValid(description),
|
||||
"description contains invalid ASCII characters, it must conform to RFC 6750");
|
||||
Assert.isTrue(isErrorCodeValid(errorCode),
|
||||
"errorCode contains invalid ASCII characters, it must conform to RFC 6750");
|
||||
Assert.isTrue(isErrorUriValid(errorUri),
|
||||
"errorUri contains invalid ASCII characters, it must conform to RFC 6750");
|
||||
Assert.isTrue(isScopeValid(scope),
|
||||
"scope contains invalid ASCII characters, it must conform to RFC 6750");
|
||||
|
||||
this.httpStatus = httpStatus;
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP status.
|
||||
* @return the HTTP status
|
||||
*/
|
||||
public HttpStatus getHttpStatus() {
|
||||
return this.httpStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the scope.
|
||||
* @return the scope
|
||||
*/
|
||||
public String getScope() {
|
||||
return this.scope;
|
||||
}
|
||||
|
||||
private static boolean isDescriptionValid(String description) {
|
||||
return description == null ||
|
||||
description.chars().allMatch(c ->
|
||||
withinTheRangeOf(c, 0x20, 0x21) ||
|
||||
withinTheRangeOf(c, 0x23, 0x5B) ||
|
||||
withinTheRangeOf(c, 0x5D, 0x7E));
|
||||
}
|
||||
|
||||
private static boolean isErrorCodeValid(String errorCode) {
|
||||
return errorCode.chars().allMatch(c ->
|
||||
withinTheRangeOf(c, 0x20, 0x21) ||
|
||||
withinTheRangeOf(c, 0x23, 0x5B) ||
|
||||
withinTheRangeOf(c, 0x5D, 0x7E));
|
||||
}
|
||||
|
||||
private static boolean isErrorUriValid(String errorUri) {
|
||||
return errorUri == null ||
|
||||
errorUri.chars().allMatch(c ->
|
||||
c == 0x21 ||
|
||||
withinTheRangeOf(c, 0x23, 0x5B) ||
|
||||
withinTheRangeOf(c, 0x5D, 0x7E));
|
||||
}
|
||||
|
||||
private static boolean isScopeValid(String scope) {
|
||||
return scope == null ||
|
||||
scope.chars().allMatch(c ->
|
||||
withinTheRangeOf(c, 0x20, 0x21) ||
|
||||
withinTheRangeOf(c, 0x23, 0x5B) ||
|
||||
withinTheRangeOf(c, 0x5D, 0x7E));
|
||||
}
|
||||
|
||||
private static boolean withinTheRangeOf(int c, int min, int max) {
|
||||
return c >= min && c <= max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource;
|
||||
|
||||
/**
|
||||
* Standard error codes defined by the OAuth 2.0 Authorization Framework: Bearer Token Usage.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 5.1
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">RFC 6750 Section 3.1: Error Codes</a>
|
||||
*/
|
||||
public interface BearerTokenErrorCodes {
|
||||
|
||||
/**
|
||||
* {@code invalid_request} - The request is missing a required parameter, includes an unsupported parameter or
|
||||
* parameter value, repeats the same parameter, uses more than one method for including an access token, or is
|
||||
* otherwise malformed.
|
||||
*/
|
||||
String INVALID_REQUEST = "invalid_request";
|
||||
|
||||
/**
|
||||
* {@code invalid_token} - The access token provided is expired, revoked, malformed, or invalid for other
|
||||
* reasons.
|
||||
*/
|
||||
String INVALID_TOKEN = "invalid_token";
|
||||
|
||||
/**
|
||||
* {@code insufficient_scope} - The request requires higher privileges than provided by the access token.
|
||||
*/
|
||||
String INSUFFICIENT_SCOPE = "insufficient_scope";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.authentication;
|
||||
|
||||
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.core.SpringSecurityCoreVersion;
|
||||
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 Joe Grandja
|
||||
* @since 5.1
|
||||
* @see OAuth2AccessToken
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/search/rfc7662#section-2.2">2.2 Introspection Response</a>
|
||||
*/
|
||||
public abstract class AbstractOAuth2TokenAuthenticationToken<T extends AbstractOAuth2Token> extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
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) {
|
||||
|
||||
super(authorities);
|
||||
|
||||
Assert.notNull(token, "token cannot be null");
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,151 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.authentication;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s
|
||||
* for protecting OAuth 2.0 Resource Servers.
|
||||
* <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 {@see Authentication} statement.
|
||||
* <p>
|
||||
* <p>
|
||||
* Scopes are translated into {@link GrantedAuthority}s according to the following algorithm:
|
||||
*
|
||||
* 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 Josh Cummings
|
||||
* @author Joe Grandja
|
||||
* @since 5.1
|
||||
* @see AuthenticationProvider
|
||||
* @see JwtDecoder
|
||||
*/
|
||||
public final class JwtAuthenticationProvider implements AuthenticationProvider {
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
|
||||
Arrays.asList("scope", "scp");
|
||||
|
||||
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
|
||||
|
||||
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" target="_blank">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;
|
||||
try {
|
||||
jwt = this.jwtDecoder.decode(bearer.getToken());
|
||||
} catch (JwtException failed) {
|
||||
OAuth2Error invalidToken;
|
||||
try {
|
||||
invalidToken = invalidToken(failed.getMessage());
|
||||
} catch ( IllegalArgumentException malformed ) {
|
||||
// some third-party library error messages are not suitable for RFC 6750's error message charset
|
||||
invalidToken = invalidToken("An error occurred while attempting to decode the Jwt: Invalid token");
|
||||
}
|
||||
throw new OAuth2AuthenticationException(invalidToken, failed);
|
||||
}
|
||||
|
||||
Collection<GrantedAuthority> authorities =
|
||||
this.getScopes(jwt)
|
||||
.stream()
|
||||
.map(authority -> SCOPE_AUTHORITY_PREFIX + authority)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
|
||||
|
||||
token.setDetails(bearer.getDetails());
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static OAuth2Error invalidToken(String message) {
|
||||
return new BearerTokenError(
|
||||
BearerTokenErrorCodes.INVALID_TOKEN,
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
message,
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
}
|
||||
|
||||
private static Collection<String> getScopes(Jwt jwt) {
|
||||
for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) {
|
||||
Object scopes = jwt.getClaims().get(attributeName);
|
||||
if (scopes instanceof String) {
|
||||
if (StringUtils.hasText((String) scopes)) {
|
||||
return Arrays.asList(((String) scopes).split(" "));
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else if (scopes instanceof Collection) {
|
||||
return (Collection<String>) scopes;
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.authentication;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.core.TransientAuthentication;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractOAuth2TokenAuthenticationToken}
|
||||
* representing a {@link Jwt} {@code Authentication}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.1
|
||||
* @see AbstractOAuth2TokenAuthenticationToken
|
||||
* @see Jwt
|
||||
*/
|
||||
@TransientAuthentication
|
||||
public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param jwt the JWT
|
||||
*/
|
||||
public JwtAuthenticationToken(Jwt jwt) {
|
||||
super(jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getTokenAttributes() {
|
||||
return this.getToken().getClaims();
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Jwt}'s subject, if any
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.getToken().getSubject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and interfaces.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.authentication;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Resource Server core classes and interfaces providing support.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource;
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests
|
||||
* using {@link BearerTokenAuthenticationFilter}.
|
||||
* <p>
|
||||
* Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate
|
||||
* {@code WWW-Authenticate} HTTP header.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 5.1
|
||||
* @see BearerTokenError
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate
|
||||
* Response Header Field</a>
|
||||
*/
|
||||
public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private String realmName;
|
||||
|
||||
/**
|
||||
* Collect error details from the provided parameters and format according to
|
||||
* RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}.
|
||||
*
|
||||
* @param request that resulted in an <code>AuthenticationException</code>
|
||||
* @param response so that the user agent can begin authentication
|
||||
* @param authException that caused the invocation
|
||||
*/
|
||||
@Override
|
||||
public void commence(
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpStatus status = HttpStatus.UNAUTHORIZED;
|
||||
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
|
||||
if (this.realmName != null) {
|
||||
parameters.put("realm", this.realmName);
|
||||
}
|
||||
|
||||
if (authException instanceof OAuth2AuthenticationException) {
|
||||
OAuth2Error error = ((OAuth2AuthenticationException) authException).getError();
|
||||
|
||||
parameters.put("error", error.getErrorCode());
|
||||
|
||||
if (StringUtils.hasText(error.getDescription())) {
|
||||
parameters.put("error_description", error.getDescription());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(error.getUri())) {
|
||||
parameters.put("error_uri", error.getUri());
|
||||
}
|
||||
|
||||
if (error instanceof BearerTokenError) {
|
||||
BearerTokenError bearerTokenError = (BearerTokenError) error;
|
||||
|
||||
if (StringUtils.hasText(bearerTokenError.getScope())) {
|
||||
parameters.put("scope", bearerTokenError.getScope());
|
||||
}
|
||||
|
||||
status = ((BearerTokenError) error).getHttpStatus();
|
||||
}
|
||||
}
|
||||
|
||||
String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
|
||||
|
||||
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
|
||||
response.setStatus(status.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default realm name to use in the bearer token error response
|
||||
*
|
||||
* @param realmName
|
||||
*/
|
||||
public final void setRealmName(String realmName) {
|
||||
this.realmName = realmName;
|
||||
}
|
||||
|
||||
private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
|
||||
String wwwAuthenticate = "Bearer";
|
||||
if (!parameters.isEmpty()) {
|
||||
wwwAuthenticate += parameters.entrySet().stream()
|
||||
.map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"")
|
||||
.collect(Collectors.joining(", ", " ", ""));
|
||||
}
|
||||
|
||||
return wwwAuthenticate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Authenticates requests that contain an OAuth 2.0
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>.
|
||||
*
|
||||
* This filter should be wired with an {@link AuthenticationManager} that can authenticate a
|
||||
* {@link BearerTokenAuthenticationToken}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Vedran Pavic
|
||||
* @author Joe Grandja
|
||||
* @since 5.1
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750" target="_blank">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
|
||||
* @see JwtAuthenticationProvider
|
||||
*/
|
||||
public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
|
||||
new WebAuthenticationDetailsSource();
|
||||
|
||||
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
|
||||
private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
|
||||
|
||||
/**
|
||||
* Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s)
|
||||
* @param authenticationManager
|
||||
*/
|
||||
public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) {
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract any <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a> from
|
||||
* the request and attempt an authentication.
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param filterChain
|
||||
* @throws ServletException
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
final boolean debug = this.logger.isDebugEnabled();
|
||||
|
||||
String token;
|
||||
|
||||
try {
|
||||
token = this.bearerTokenResolver.resolve(request);
|
||||
} catch ( OAuth2AuthenticationException invalid ) {
|
||||
this.authenticationEntryPoint.commence(request, response, invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
|
||||
|
||||
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
|
||||
try {
|
||||
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
|
||||
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(authenticationResult);
|
||||
SecurityContextHolder.setContext(context);
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
} catch (AuthenticationException failed) {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
if (debug) {
|
||||
this.logger.debug("Authentication request for failed: " + failed);
|
||||
}
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response, failed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link BearerTokenResolver} to use. Defaults to {@link DefaultBearerTokenResolver}.
|
||||
* @param bearerTokenResolver the {@code BearerTokenResolver} to use
|
||||
*/
|
||||
public final void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
|
||||
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
|
||||
this.bearerTokenResolver = bearerTokenResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link AuthenticationEntryPoint} to use. Defaults to {@link BearerTokenAuthenticationEntryPoint}.
|
||||
* @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use
|
||||
*/
|
||||
public final void setAuthenticationEntryPoint(final AuthenticationEntryPoint authenticationEntryPoint) {
|
||||
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
|
||||
this.authenticationEntryPoint = authenticationEntryPoint;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
|
||||
/**
|
||||
* A strategy for resolving <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s
|
||||
* from the {@link HttpServletRequest}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 5.1
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750 Section 2: Authenticated Requests</a>
|
||||
*/
|
||||
public interface BearerTokenResolver {
|
||||
|
||||
/**
|
||||
* Resolve any <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>
|
||||
* value from the request.
|
||||
*
|
||||
* @param request the request
|
||||
* @return the Bearer Token value or {@code null} if none found
|
||||
* @throws OAuth2AuthenticationException if the found token is invalid
|
||||
*/
|
||||
String resolve(HttpServletRequest request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The default {@link BearerTokenResolver} implementation based on RFC 6750.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 5.1
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750 Section 2: Authenticated Requests</a>
|
||||
*/
|
||||
public final class DefaultBearerTokenResolver implements BearerTokenResolver {
|
||||
|
||||
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$");
|
||||
|
||||
private boolean allowFormEncodedBodyParameter = false;
|
||||
|
||||
private boolean allowUriQueryParameter = false;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String resolve(HttpServletRequest request) {
|
||||
String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
|
||||
String parameterToken = resolveFromRequestParameters(request);
|
||||
if (authorizationHeaderToken != null) {
|
||||
if (parameterToken != null) {
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Found multiple bearer tokens in the request",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return authorizationHeaderToken;
|
||||
}
|
||||
else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
|
||||
return parameterToken;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if transport of access token using form-encoded body parameter is supported. Defaults to {@code false}.
|
||||
* @param allowFormEncodedBodyParameter if the form-encoded body parameter is supported
|
||||
*/
|
||||
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
|
||||
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if transport of access token using URI query parameter is supported. Defaults to {@code false}.
|
||||
*
|
||||
* The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as
|
||||
* stating that it was only included for completeness.
|
||||
*
|
||||
* @param allowUriQueryParameter if the URI query parameter is supported
|
||||
*/
|
||||
public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
|
||||
this.allowUriQueryParameter = allowUriQueryParameter;
|
||||
}
|
||||
|
||||
private static String resolveFromAuthorizationHeader(HttpServletRequest request) {
|
||||
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) {
|
||||
Matcher matcher = authorizationPattern.matcher(authorization);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN,
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"Bearer token is malformed",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
return matcher.group("token");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String resolveFromRequestParameters(HttpServletRequest request) {
|
||||
String[] values = request.getParameterValues("access_token");
|
||||
if (values == null || values.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (values.length == 1) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Found multiple bearer tokens in the request",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private boolean isParameterTokenSupportedForRequest(HttpServletRequest request) {
|
||||
return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod()))
|
||||
|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web.access;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Translates any {@link AccessDeniedException} into an HTTP response in accordance with
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate</a>.
|
||||
*
|
||||
* So long as the class can prove that the request has a valid OAuth 2.0 {@link Authentication}, then will return an
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">insufficient scope error</a>; otherwise,
|
||||
* it will simply indicate the scheme (Bearer) and any configured realm.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.1
|
||||
*/
|
||||
public final class BearerTokenAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
|
||||
Arrays.asList("scope", "scp");
|
||||
|
||||
private String realmName;
|
||||
|
||||
/**
|
||||
* Collect error details from the provided parameters and format according to
|
||||
* RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}.
|
||||
*
|
||||
* @param request that resulted in an <code>AccessDeniedException</code>
|
||||
* @param response so that the user agent can be advised of the failure
|
||||
* @param accessDeniedException that caused the invocation
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void handle(
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException)
|
||||
throws IOException, ServletException {
|
||||
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
|
||||
if (this.realmName != null) {
|
||||
parameters.put("realm", this.realmName);
|
||||
}
|
||||
|
||||
if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) {
|
||||
AbstractOAuth2TokenAuthenticationToken token =
|
||||
(AbstractOAuth2TokenAuthenticationToken) request.getUserPrincipal();
|
||||
|
||||
String scope = getScope(token);
|
||||
|
||||
parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
|
||||
parameters.put("error_description",
|
||||
String.format("The token provided has insufficient scope [%s] for this request", scope));
|
||||
parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
|
||||
if (StringUtils.hasText(scope)) {
|
||||
parameters.put("scope", scope);
|
||||
}
|
||||
}
|
||||
|
||||
String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
|
||||
|
||||
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default realm name to use in the bearer token error response
|
||||
*
|
||||
* @param realmName
|
||||
*/
|
||||
public final void setRealmName(String realmName) {
|
||||
this.realmName = realmName;
|
||||
}
|
||||
|
||||
private static String getScope(AbstractOAuth2TokenAuthenticationToken token) {
|
||||
|
||||
Map<String, Object> attributes = token.getTokenAttributes();
|
||||
|
||||
for (String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES) {
|
||||
Object scopes = attributes.get(attributeName);
|
||||
if (scopes instanceof String) {
|
||||
return (String) scopes;
|
||||
} else if (scopes instanceof Collection) {
|
||||
Collection coll = (Collection) scopes;
|
||||
return (String) coll.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
|
||||
String wwwAuthenticate = "Bearer";
|
||||
if (!parameters.isEmpty()) {
|
||||
wwwAuthenticate += parameters.entrySet().stream()
|
||||
.map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"")
|
||||
.collect(Collectors.joining(", ", " ", ""));
|
||||
}
|
||||
|
||||
return wwwAuthenticate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Resource Server access denial classes and interfaces.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.web.access;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenAuthenticationToken}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class BearerTokenAuthenticationTokenTests {
|
||||
@Test
|
||||
public void constructorWhenTokenIsNullThenThrowsException() {
|
||||
assertThatCode(() -> new BearerTokenAuthenticationToken(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("token cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenTokenIsEmptyThenThrowsException() {
|
||||
assertThatCode(() -> new BearerTokenAuthenticationToken(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenError}
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class BearerTokenErrorTests {
|
||||
|
||||
private static final String TEST_ERROR_CODE = "test-code";
|
||||
|
||||
private static final HttpStatus TEST_HTTP_STATUS = HttpStatus.UNAUTHORIZED;
|
||||
|
||||
private static final String TEST_DESCRIPTION = "test-description";
|
||||
|
||||
private static final String TEST_URI = "http://example.com";
|
||||
|
||||
private static final String TEST_SCOPE = "test-scope";
|
||||
|
||||
@Test
|
||||
public void constructorWithErrorCodeWhenErrorCodeIsValidThenCreated() {
|
||||
BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, null, null);
|
||||
|
||||
assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE);
|
||||
assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS);
|
||||
assertThat(error.getDescription()).isNull();
|
||||
assertThat(error.getUri()).isNull();
|
||||
assertThat(error.getScope()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, null, null))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, null, null))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithErrorCodeAndHttpStatusWhenHttpStatusIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, null, null))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenAllParametersAreValidThenCreated() {
|
||||
BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI,
|
||||
TEST_SCOPE);
|
||||
|
||||
assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE);
|
||||
assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS);
|
||||
assertThat(error.getDescription()).isEqualTo(TEST_DESCRIPTION);
|
||||
assertThat(error.getUri()).isEqualTo(TEST_URI);
|
||||
assertThat(error.getScope()).isEqualTo(TEST_SCOPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenErrorCodeIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenHttpStatusIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenErrorCodeIsInvalidThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE + "\"", TEST_HTTP_STATUS, TEST_DESCRIPTION,
|
||||
TEST_URI, TEST_SCOPE))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("errorCode")
|
||||
.hasMessageContaining("RFC 6750");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenDescriptionIsInvalidThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION + "\"",
|
||||
TEST_URI, TEST_SCOPE))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("description")
|
||||
.hasMessageContaining("RFC 6750");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenErrorUriIsInvalidThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION,
|
||||
TEST_URI + "\"", TEST_SCOPE))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("errorUri")
|
||||
.hasMessageContaining("RFC 6750");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithAllParametersWhenScopeIsInvalidThenThrowIllegalArgumentException() {
|
||||
assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION,
|
||||
TEST_URI, TEST_SCOPE + "\""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("scope")
|
||||
.hasMessageContaining("RFC 6750");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtAuthenticationProvider}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class JwtAuthenticationProviderTests {
|
||||
@Mock
|
||||
JwtDecoder jwtDecoder;
|
||||
|
||||
JwtAuthenticationProvider provider;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.provider =
|
||||
new JwtAuthenticationProvider(this.jwtDecoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenJwtDecodesThenAuthenticationHasAttributesContainedInJwt() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("name", "value");
|
||||
Jwt jwt = this.jwt(claims);
|
||||
|
||||
when(this.jwtDecoder.decode("token")).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
assertThat(authentication.getTokenAttributes()).isEqualTo(claims);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenJwtDecodeFailsThenRespondsWithInvalidToken() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
when(this.jwtDecoder.decode("token")).thenThrow(JwtException.class);
|
||||
|
||||
assertThatCode(() -> this.provider.authenticate(token))
|
||||
.matches(failed -> failed instanceof OAuth2AuthenticationException)
|
||||
.matches(errorCode(BearerTokenErrorCodes.INVALID_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Jwt jwt = this.jwt(Maps.newHashMap("scope", "message:read message:write"));
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
Collection<GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
|
||||
assertThat(authorities).containsExactly(
|
||||
new SimpleGrantedAuthority("SCOPE_message:read"),
|
||||
new SimpleGrantedAuthority("SCOPE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Jwt jwt = this.jwt(Maps.newHashMap("scope", ""));
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
Collection<GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
|
||||
assertThat(authorities).containsExactly();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")));
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
Collection<GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
|
||||
assertThat(authorities).containsExactly(
|
||||
new SimpleGrantedAuthority("SCOPE_message:read"),
|
||||
new SimpleGrantedAuthority("SCOPE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList()));
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
Collection<GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
|
||||
assertThat(authorities).containsExactly();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenHasBothScopeAndScpThenScopeAttributeIsTranslatedToAuthorities() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Map<String, Object> claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
|
||||
claims.put("scope", "missive:read missive:write");
|
||||
Jwt jwt = this.jwt(claims);
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
Collection<GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
|
||||
assertThat(authorities).containsExactly(
|
||||
new SimpleGrantedAuthority("SCOPE_missive:read"),
|
||||
new SimpleGrantedAuthority("SCOPE_missive:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
Map<String, Object> claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
|
||||
claims.put("scope", "");
|
||||
Jwt jwt = this.jwt(claims);
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt);
|
||||
|
||||
JwtAuthenticationToken authentication =
|
||||
(JwtAuthenticationToken) this.provider.authenticate(token);
|
||||
|
||||
Collection<GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
|
||||
assertThat(authorities).containsExactly();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenDecoderThrowsIncompatibleErrorMessageThenWrapsWithGenericOne() {
|
||||
BearerTokenAuthenticationToken token = this.authentication();
|
||||
|
||||
when(this.jwtDecoder.decode(token.getToken())).thenThrow(new JwtException("with \"invalid\" chars"));
|
||||
|
||||
assertThatCode(() -> this.provider.authenticate(token))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasFieldOrPropertyWithValue(
|
||||
"error.description",
|
||||
"An error occurred while attempting to decode the Jwt: Invalid token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenBearerTokenAuthenticationTokenThenReturnsTrue() {
|
||||
assertThat(this.provider.supports(BearerTokenAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
private BearerTokenAuthenticationToken authentication() {
|
||||
return new BearerTokenAuthenticationToken("token");
|
||||
}
|
||||
|
||||
private Jwt jwt(Map<String, Object> claims) {
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
headers.put("alg", JwsAlgorithms.RS256);
|
||||
|
||||
return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims);
|
||||
}
|
||||
|
||||
private Predicate<? super Throwable> errorCode(String errorCode) {
|
||||
return failed ->
|
||||
((OAuth2AuthenticationException) failed).getError().getErrorCode() == errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtAuthenticationToken}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class JwtAuthenticationTokenTests {
|
||||
|
||||
@Test
|
||||
public void getNameWhenJwtHasSubjectThenReturnsSubject() {
|
||||
Jwt jwt = this.jwt(Maps.newHashMap("sub", "Carl"));
|
||||
|
||||
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
|
||||
|
||||
assertThat(token.getName()).isEqualTo("Carl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNameWhenJwtHasNoSubjectThenReturnsNull() {
|
||||
Jwt jwt = this.jwt(Maps.newHashMap("claim", "value"));
|
||||
|
||||
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
|
||||
|
||||
assertThat(token.getName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenJwtIsNullThenThrowsException() {
|
||||
assertThatCode(() -> new JwtAuthenticationToken(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("token cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenUsingCorrectParametersThenConstructedCorrectly() {
|
||||
Collection authorities = Arrays.asList(new SimpleGrantedAuthority("test"));
|
||||
Map claims = Maps.newHashMap("claim", "value");
|
||||
Jwt jwt = this.jwt(claims);
|
||||
|
||||
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
|
||||
|
||||
assertThat(token.getAuthorities()).isEqualTo(authorities);
|
||||
assertThat(token.getPrincipal()).isEqualTo(jwt);
|
||||
assertThat(token.getCredentials()).isEqualTo(jwt);
|
||||
assertThat(token.getToken()).isEqualTo(jwt);
|
||||
assertThat(token.getTokenAttributes()).isEqualTo(claims);
|
||||
assertThat(token.isAuthenticated()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenUsingOnlyJwtThenConstructedCorrectly() {
|
||||
Map claims = Maps.newHashMap("claim", "value");
|
||||
Jwt jwt = this.jwt(claims);
|
||||
|
||||
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
|
||||
|
||||
assertThat(token.getAuthorities()).isEmpty();
|
||||
assertThat(token.getPrincipal()).isEqualTo(jwt);
|
||||
assertThat(token.getCredentials()).isEqualTo(jwt);
|
||||
assertThat(token.getToken()).isEqualTo(jwt);
|
||||
assertThat(token.getTokenAttributes()).isEqualTo(claims);
|
||||
assertThat(token.isAuthenticated()).isFalse();
|
||||
}
|
||||
|
||||
private Jwt jwt(Map<String, Object> claims) {
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
headers.put("alg", JwsAlgorithms.RS256);
|
||||
|
||||
return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenAuthenticationEntryPoint}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class BearerTokenAuthenticationEntryPointTests {
|
||||
|
||||
private BearerTokenAuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenNoBearerTokenErrorThenStatus401AndAuthHeader()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test"));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenNoBearerTokenErrorAndRealmSetThenStatus401AndAuthHeaderWithRealm()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
this.authenticationEntryPoint.setRealmName("test");
|
||||
this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test"));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithError()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(
|
||||
BearerTokenErrorCodes.INVALID_REQUEST,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
null,
|
||||
null);
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_request\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorDetails()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST,
|
||||
"The access token expired", null, null);
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.getHeader("WWW-Authenticate"))
|
||||
.isEqualTo("Bearer error=\"invalid_request\", error_description=\"The access token expired\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorUri()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST,
|
||||
null, "http://example.com", null);
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.getHeader("WWW-Authenticate"))
|
||||
.isEqualTo("Bearer error=\"invalid_request\", error_uri=\"http://example.com\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInvalidTokenErrorThenStatus401AndHeaderWithError()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED,
|
||||
null, null);
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_token\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithError()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN,
|
||||
null, null);
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithErrorAndScope()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN,
|
||||
null, null, "test.read test.write");
|
||||
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate"))
|
||||
.isEqualTo("Bearer error=\"insufficient_scope\", scope=\"test.read test.write\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commenceWhenInsufficientScopeAndRealmSetThenStatus403AndHeaderWithErrorAndAllDetails()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN,
|
||||
"Insufficient scope", "http://example.com", "test.read test.write");
|
||||
|
||||
this.authenticationEntryPoint.setRealmName("test");
|
||||
this.authenticationEntryPoint.commence(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo(
|
||||
"Bearer realm=\"test\", error=\"insufficient_scope\", error_description=\"Insufficient scope\", "
|
||||
+ "error_uri=\"http://example.com\", scope=\"test.read test.write\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() {
|
||||
assertThatCode(() -> this.authenticationEntryPoint.setRealmName(null))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests {@link BearerTokenAuthenticationFilterTests}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class BearerTokenAuthenticationFilterTests {
|
||||
@Mock
|
||||
AuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
||||
@Mock
|
||||
AuthenticationManager authenticationManager;
|
||||
|
||||
@Mock
|
||||
BearerTokenResolver bearerTokenResolver;
|
||||
|
||||
MockHttpServletRequest request;
|
||||
|
||||
MockHttpServletResponse response;
|
||||
|
||||
MockFilterChain filterChain;
|
||||
|
||||
@InjectMocks
|
||||
BearerTokenAuthenticationFilter filter;
|
||||
|
||||
@Before
|
||||
public void httpMocks() {
|
||||
this.request = new MockHttpServletRequest();
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.filterChain = new MockFilterChain();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setterMocks() {
|
||||
this.filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
|
||||
this.filter.setBearerTokenResolver(this.bearerTokenResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenBearerTokenPresentThenAuthenticates() throws ServletException, IOException {
|
||||
when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token");
|
||||
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
|
||||
ArgumentCaptor<BearerTokenAuthenticationToken> captor =
|
||||
ArgumentCaptor.forClass(BearerTokenAuthenticationToken.class);
|
||||
|
||||
verify(this.authenticationManager).authenticate(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getPrincipal()).isEqualTo("token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNoBearerTokenPresentThenDoesNotAuthenticate()
|
||||
throws ServletException, IOException {
|
||||
|
||||
when(this.bearerTokenResolver.resolve(this.request)).thenReturn(null);
|
||||
|
||||
dontAuthenticate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenMalformedBearerTokenThenPropagatesError() throws ServletException, IOException {
|
||||
BearerTokenError error = new BearerTokenError(
|
||||
BearerTokenErrorCodes.INVALID_REQUEST,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"description",
|
||||
"uri");
|
||||
|
||||
OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error);
|
||||
|
||||
when(this.bearerTokenResolver.resolve(this.request)).thenThrow(exception);
|
||||
|
||||
dontAuthenticate();
|
||||
|
||||
verify(this.authenticationEntryPoint).commence(this.request, this.response, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthenticationFailsThenPropagatesError() throws ServletException, IOException {
|
||||
BearerTokenError error = new BearerTokenError(
|
||||
BearerTokenErrorCodes.INVALID_TOKEN,
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"description",
|
||||
"uri"
|
||||
);
|
||||
|
||||
OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error);
|
||||
|
||||
when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token");
|
||||
when(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class)))
|
||||
.thenThrow(exception);
|
||||
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
|
||||
verify(this.authenticationEntryPoint).commence(this.request, this.response, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationEntryPointWhenNullThenThrowsException() {
|
||||
assertThatCode(() -> this.filter.setAuthenticationEntryPoint(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("authenticationEntryPoint cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setBearerTokenResolverWhenNullThenThrowsException() {
|
||||
assertThatCode(() -> this.filter.setBearerTokenResolver(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("bearerTokenResolver cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNullAuthenticationManagerThenThrowsException() {
|
||||
assertThatCode(() -> new BearerTokenAuthenticationFilter(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("authenticationManager cannot be null");
|
||||
}
|
||||
|
||||
private void dontAuthenticate()
|
||||
throws ServletException, IOException {
|
||||
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
|
||||
verifyNoMoreInteractions(this.authenticationManager);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultBearerTokenResolver}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public class DefaultBearerTokenResolverTests {
|
||||
|
||||
private static final String TEST_TOKEN = "test-token";
|
||||
|
||||
private DefaultBearerTokenResolver resolver;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.resolver = new DefaultBearerTokenResolver();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes()));
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer ");
|
||||
|
||||
assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining(("Bearer token is malformed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer an\"invalid\"token");
|
||||
|
||||
assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining(("Bearer token is malformed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
|
||||
request.setMethod("POST");
|
||||
request.setContentType("application/x-www-form-urlencoded");
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Found multiple bearer tokens in the request");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
|
||||
request.setMethod("GET");
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Found multiple bearer tokens in the request");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenRequestContainsTwoAccessTokenParametersThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addParameter("access_token", "token1", "token2");
|
||||
|
||||
assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Found multiple bearer tokens in the request");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved() {
|
||||
this.resolver.setAllowFormEncodedBodyParameter(true);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("POST");
|
||||
request.setContentType("application/x-www-form-urlencoded");
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("POST");
|
||||
request.setContentType("application/x-www-form-urlencoded");
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() {
|
||||
this.resolver.setAllowUriQueryParameter(true);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("GET");
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("GET");
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web.access;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenAccessDeniedHandlerTests}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class BearerTokenAccessDeniedHandlerTests {
|
||||
private BearerTokenAccessDeniedHandler accessDeniedHandler;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.accessDeniedHandler = new BearerTokenAccessDeniedHandler();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenNotOAuth2AuthenticatedThenStatus403()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "pass");
|
||||
request.setUserPrincipal(authentication);
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenNotOAuth2AuthenticatedAndRealmSetThenStatus403AndAuthHeaderWithRealm()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "pass");
|
||||
request.setUserPrincipal(authentication);
|
||||
|
||||
this.accessDeniedHandler.setRealmName("test");
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasNoScopesThenInsufficientScopeError()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap());
|
||||
request.setUserPrincipal(token);
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasScopeAttributeThenInsufficientScopeErrorWithScopes()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Map<String, Object> attributes = Maps.newHashMap("scope", "message:read message:write");
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
|
||||
request.setUserPrincipal(token);
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
|
||||
"scope=\"message:read message:write\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasEmptyScopeAttributeThenInsufficientScopeError()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Map<String, Object> attributes = Maps.newHashMap("scope", "");
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
|
||||
request.setUserPrincipal(token);
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasScpAttributeThenInsufficientScopeErrorWithScopes()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Map<String, Object> attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
|
||||
request.setUserPrincipal(token);
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
|
||||
"scope=\"message:read message:write\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasEmptyScpAttributeThenInsufficientScopeError()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Map<String, Object> attributes = Maps.newHashMap("scp", Collections.emptyList());
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
|
||||
request.setUserPrincipal(token);
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasBothScopeAndScpAttributesTheInsufficientErrorBasedOnScopeAttribute()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Map<String, Object> attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"));
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
|
||||
request.setUserPrincipal(token);
|
||||
attributes.put("scope", "missive:read missive:write");
|
||||
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [missive:read missive:write] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
|
||||
"scope=\"missive:read missive:write\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenTokenHasScopeAttributeAndRealmIsSetThenInsufficientScopeErrorWithScopesAndRealm()
|
||||
throws Exception {
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Map<String, Object> attributes = Maps.newHashMap("scope", "message:read message:write");
|
||||
Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes);
|
||||
request.setUserPrincipal(token);
|
||||
|
||||
this.accessDeniedHandler.setRealmName("test");
|
||||
this.accessDeniedHandler.handle(request, response, null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\", " +
|
||||
"error=\"insufficient_scope\", " +
|
||||
"error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " +
|
||||
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " +
|
||||
"scope=\"message:read message:write\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() {
|
||||
assertThatCode(() -> this.accessDeniedHandler.setRealmName(null))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
static class TestingOAuth2TokenAuthenticationToken
|
||||
extends AbstractOAuth2TokenAuthenticationToken<TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token> {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
protected TestingOAuth2TokenAuthenticationToken(Map<String, Object> attributes) {
|
||||
super(new TestingOAuth2Token("token"));
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTokenAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
static class TestingOAuth2Token extends AbstractOAuth2Token {
|
||||
public TestingOAuth2Token(String tokenValue) {
|
||||
super(tokenValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user