|
@@ -16,14 +16,20 @@
|
|
|
|
|
|
package org.springframework.security.oauth2.server.resource.web.server.authentication;
|
|
package org.springframework.security.oauth2.server.resource.web.server.authentication;
|
|
|
|
|
|
|
|
+import static org.springframework.security.oauth2.server.resource.BearerTokenErrors.invalidRequest;
|
|
|
|
+
|
|
import java.util.List;
|
|
import java.util.List;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
+import reactor.core.publisher.Flux;
|
|
import reactor.core.publisher.Mono;
|
|
import reactor.core.publisher.Mono;
|
|
|
|
+import reactor.util.function.Tuple2;
|
|
|
|
+import reactor.util.function.Tuples;
|
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
|
import org.springframework.http.HttpHeaders;
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.HttpMethod;
|
|
|
|
+import org.springframework.http.MediaType;
|
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|
@@ -47,16 +53,20 @@ import org.springframework.web.server.ServerWebExchange;
|
|
*/
|
|
*/
|
|
public class ServerBearerTokenAuthenticationConverter implements ServerAuthenticationConverter {
|
|
public class ServerBearerTokenAuthenticationConverter implements ServerAuthenticationConverter {
|
|
|
|
|
|
|
|
+ public static final String ACCESS_TOKEN_NAME = "access_token";
|
|
|
|
+ public static final String MULTIPLE_BEARER_TOKENS_ERROR_MSG = "Found multiple bearer tokens in the request";
|
|
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
|
|
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
|
|
Pattern.CASE_INSENSITIVE);
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
|
|
private boolean allowUriQueryParameter = false;
|
|
private boolean allowUriQueryParameter = false;
|
|
|
|
|
|
|
|
+ private boolean allowFormEncodedBodyParameter = false;
|
|
|
|
+
|
|
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
|
|
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
|
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
|
- return Mono.fromCallable(() -> token(exchange.getRequest())).map((token) -> {
|
|
|
|
|
|
+ return Mono.defer(() -> token(exchange)).map(token -> {
|
|
if (token.isEmpty()) {
|
|
if (token.isEmpty()) {
|
|
BearerTokenError error = invalidTokenError();
|
|
BearerTokenError error = invalidTokenError();
|
|
throw new OAuth2AuthenticationException(error);
|
|
throw new OAuth2AuthenticationException(error);
|
|
@@ -65,43 +75,53 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
- private String token(ServerHttpRequest request) {
|
|
|
|
- String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
|
|
|
|
- String parameterToken = resolveAccessTokenFromRequest(request);
|
|
|
|
-
|
|
|
|
- if (authorizationHeaderToken != null) {
|
|
|
|
- if (parameterToken != null) {
|
|
|
|
- BearerTokenError error = BearerTokenErrors
|
|
|
|
- .invalidRequest("Found multiple bearer tokens in the request");
|
|
|
|
- throw new OAuth2AuthenticationException(error);
|
|
|
|
- }
|
|
|
|
- return authorizationHeaderToken;
|
|
|
|
- }
|
|
|
|
- if (parameterToken != null && !StringUtils.hasText(parameterToken)) {
|
|
|
|
- BearerTokenError error = BearerTokenErrors
|
|
|
|
- .invalidRequest("The requested token parameter is an empty string");
|
|
|
|
- throw new OAuth2AuthenticationException(error);
|
|
|
|
- }
|
|
|
|
- return parameterToken;
|
|
|
|
|
|
+ private Mono<String> token(ServerWebExchange exchange) {
|
|
|
|
+ final ServerHttpRequest request = exchange.getRequest();
|
|
|
|
+
|
|
|
|
+ return Flux.merge(resolveFromAuthorizationHeader(request.getHeaders()).map(s -> Tuples.of(s, TokenSource.HEADER)),
|
|
|
|
+ resolveAccessTokenFromRequest(request).map(s -> Tuples.of(s, TokenSource.QUERY_PARAMETER)),
|
|
|
|
+ resolveAccessTokenFromBody(exchange).map(s -> Tuples.of(s, TokenSource.BODY_PARAMETER)))
|
|
|
|
+ .collectList()
|
|
|
|
+ .mapNotNull(tokenTuples -> {
|
|
|
|
+ switch (tokenTuples.size()) {
|
|
|
|
+ case 0:
|
|
|
|
+ return null;
|
|
|
|
+ case 1:
|
|
|
|
+ return getTokenIfSupported(tokenTuples.get(0), request);
|
|
|
|
+ default:
|
|
|
|
+ BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
|
|
|
|
+ throw new OAuth2AuthenticationException(error);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
}
|
|
}
|
|
|
|
|
|
- private String resolveAccessTokenFromRequest(ServerHttpRequest request) {
|
|
|
|
- if (!isParameterTokenSupportedForRequest(request)) {
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
- List<String> parameterTokens = request.getQueryParams().get("access_token");
|
|
|
|
|
|
+ private static Mono<String> resolveAccessTokenFromRequest(ServerHttpRequest request) {
|
|
|
|
+ List<String> parameterTokens = request.getQueryParams().get(ACCESS_TOKEN_NAME);
|
|
if (CollectionUtils.isEmpty(parameterTokens)) {
|
|
if (CollectionUtils.isEmpty(parameterTokens)) {
|
|
- return null;
|
|
|
|
|
|
+ return Mono.empty();
|
|
}
|
|
}
|
|
if (parameterTokens.size() == 1) {
|
|
if (parameterTokens.size() == 1) {
|
|
- return parameterTokens.get(0);
|
|
|
|
|
|
+ return Mono.just(parameterTokens.get(0));
|
|
}
|
|
}
|
|
|
|
|
|
- BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
|
|
|
|
|
|
+ BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
|
|
throw new OAuth2AuthenticationException(error);
|
|
throw new OAuth2AuthenticationException(error);
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private String getTokenIfSupported(Tuple2<String, TokenSource> tokenTuple, ServerHttpRequest request) {
|
|
|
|
+ switch (tokenTuple.getT2()) {
|
|
|
|
+ case HEADER:
|
|
|
|
+ return tokenTuple.getT1();
|
|
|
|
+ case QUERY_PARAMETER:
|
|
|
|
+ return isParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
|
|
|
|
+ case BODY_PARAMETER:
|
|
|
|
+ return isBodyParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
|
|
|
|
+ default:
|
|
|
|
+ throw new IllegalArgumentException();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* Set if transport of access token using URI query parameter is supported. Defaults
|
|
* Set if transport of access token using URI query parameter is supported. Defaults
|
|
* to {@code false}.
|
|
* to {@code false}.
|
|
@@ -127,25 +147,70 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
|
|
this.bearerTokenHeaderName = bearerTokenHeaderName;
|
|
this.bearerTokenHeaderName = bearerTokenHeaderName;
|
|
}
|
|
}
|
|
|
|
|
|
- private String resolveFromAuthorizationHeader(HttpHeaders headers) {
|
|
|
|
|
|
+ /**
|
|
|
|
+ * 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
|
|
|
|
+ * @since 6.5
|
|
|
|
+ */
|
|
|
|
+ public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
|
|
|
|
+ this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private Mono<String> resolveFromAuthorizationHeader(HttpHeaders headers) {
|
|
String authorization = headers.getFirst(this.bearerTokenHeaderName);
|
|
String authorization = headers.getFirst(this.bearerTokenHeaderName);
|
|
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
|
|
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
|
|
- return null;
|
|
|
|
|
|
+ return Mono.empty();
|
|
}
|
|
}
|
|
Matcher matcher = authorizationPattern.matcher(authorization);
|
|
Matcher matcher = authorizationPattern.matcher(authorization);
|
|
if (!matcher.matches()) {
|
|
if (!matcher.matches()) {
|
|
BearerTokenError error = invalidTokenError();
|
|
BearerTokenError error = invalidTokenError();
|
|
throw new OAuth2AuthenticationException(error);
|
|
throw new OAuth2AuthenticationException(error);
|
|
}
|
|
}
|
|
- return matcher.group("token");
|
|
|
|
|
|
+ return Mono.just(matcher.group("token"));
|
|
}
|
|
}
|
|
|
|
|
|
private static BearerTokenError invalidTokenError() {
|
|
private static BearerTokenError invalidTokenError() {
|
|
return BearerTokenErrors.invalidToken("Bearer token is malformed");
|
|
return BearerTokenErrors.invalidToken("Bearer token is malformed");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private Mono<String> resolveAccessTokenFromBody(ServerWebExchange exchange) {
|
|
|
|
+ if (!allowFormEncodedBodyParameter) {
|
|
|
|
+ return Mono.empty();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ final ServerHttpRequest request = exchange.getRequest();
|
|
|
|
+
|
|
|
|
+ if (request.getMethod() == HttpMethod.POST &&
|
|
|
|
+ MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(request.getHeaders().getContentType())) {
|
|
|
|
+
|
|
|
|
+ return exchange.getFormData().mapNotNull(formData -> {
|
|
|
|
+ if (formData.isEmpty()) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ final List<String> tokens = formData.get(ACCESS_TOKEN_NAME);
|
|
|
|
+ if (tokens == null) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ if (tokens.size() > 1) {
|
|
|
|
+ BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
|
|
|
|
+ throw new OAuth2AuthenticationException(error);
|
|
|
|
+ }
|
|
|
|
+ return formData.getFirst(ACCESS_TOKEN_NAME);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ return Mono.empty();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private boolean isBodyParameterTokenSupportedForRequest(ServerHttpRequest request) {
|
|
|
|
+ return this.allowFormEncodedBodyParameter && HttpMethod.POST == request.getMethod();
|
|
|
|
+ }
|
|
|
|
+
|
|
private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
|
|
private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
|
|
return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
|
|
return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private enum TokenSource {HEADER, QUERY_PARAMETER, BODY_PARAMETER}
|
|
|
|
+
|
|
}
|
|
}
|