Skip to content

Commit 0170226

Browse files
Max BatischevMax Batischev
Max Batischev
authored and
Max Batischev
committed
Add BearerTokenAuthenticationConverter
Closes spring-projectsgh-14750
1 parent e771267 commit 0170226

File tree

4 files changed

+381
-97
lines changed

4 files changed

+381
-97
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.server.resource.web;
18+
19+
import java.util.List;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
22+
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpMethod;
25+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
26+
import org.springframework.security.oauth2.server.resource.BearerTokenError;
27+
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
28+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
29+
import org.springframework.util.CollectionUtils;
30+
import org.springframework.util.StringUtils;
31+
32+
/**
33+
* Base class for bearer token converter implementations.
34+
*
35+
* @author Max Batischev
36+
* @author Rob Winch
37+
* @since 6.3
38+
*/
39+
public abstract class AbstractBearerTokenAuthenticationConverter<R> {
40+
41+
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
42+
Pattern.CASE_INSENSITIVE);
43+
44+
private boolean allowUriQueryParameter = false;
45+
46+
protected String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
47+
48+
protected String token(R request) {
49+
String authorizationHeaderToken = resolveAuthorizationHeaderToken(request);
50+
String parameterToken = resolveAccessTokenFromRequest(request);
51+
52+
if (authorizationHeaderToken != null) {
53+
if (parameterToken != null) {
54+
BearerTokenError error = BearerTokenErrors
55+
.invalidRequest("Found multiple bearer tokens in the request");
56+
throw new OAuth2AuthenticationException(error);
57+
}
58+
return authorizationHeaderToken;
59+
}
60+
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
61+
return parameterToken;
62+
}
63+
return null;
64+
}
65+
66+
protected BearerTokenAuthenticationToken convertBearerToken(String token) {
67+
if (token.isEmpty()) {
68+
BearerTokenError error = invalidTokenError();
69+
throw new OAuth2AuthenticationException(error);
70+
}
71+
return new BearerTokenAuthenticationToken(token);
72+
}
73+
74+
protected abstract String resolveAuthorizationHeaderToken(R request);
75+
76+
private String resolveAccessTokenFromRequest(R request) {
77+
List<String> parameterTokens = resolveParameterTokens(request);
78+
if (CollectionUtils.isEmpty(parameterTokens)) {
79+
return null;
80+
}
81+
if (parameterTokens.size() == 1) {
82+
return parameterTokens.get(0);
83+
}
84+
85+
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
86+
throw new OAuth2AuthenticationException(error);
87+
88+
}
89+
90+
protected abstract List<String> resolveParameterTokens(R request);
91+
92+
/**
93+
* Set if transport of access token using URI query parameter is supported. Defaults
94+
* to {@code false}.
95+
* <p>
96+
* The spec recommends against using this mechanism for sending bearer tokens, and
97+
* even goes as far as stating that it was only included for completeness.
98+
* @param allowUriQueryParameter if the URI query parameter is supported
99+
*/
100+
public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
101+
this.allowUriQueryParameter = allowUriQueryParameter;
102+
}
103+
104+
/**
105+
* Set this value to configure what header is checked when resolving a Bearer Token.
106+
* This value is defaulted to {@link HttpHeaders#AUTHORIZATION}.
107+
* <p>
108+
* This allows other headers to be used as the Bearer Token source such as
109+
* {@link HttpHeaders#PROXY_AUTHORIZATION}
110+
* @param bearerTokenHeaderName the header to check when retrieving the Bearer Token.
111+
* @since 5.4
112+
*/
113+
public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
114+
this.bearerTokenHeaderName = bearerTokenHeaderName;
115+
}
116+
117+
protected String resolveFromAuthorizationHeader(String authorization) {
118+
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
119+
return null;
120+
}
121+
Matcher matcher = authorizationPattern.matcher(authorization);
122+
if (!matcher.matches()) {
123+
BearerTokenError error = invalidTokenError();
124+
throw new OAuth2AuthenticationException(error);
125+
}
126+
return matcher.group("token");
127+
}
128+
129+
protected BearerTokenError invalidTokenError() {
130+
return BearerTokenErrors.invalidToken("Bearer token is malformed");
131+
}
132+
133+
private boolean isParameterTokenSupportedForRequest(R request) {
134+
return this.allowUriQueryParameter && HttpMethod.GET.equals(getHttpMethod(request));
135+
}
136+
137+
protected abstract HttpMethod getHttpMethod(R request);
138+
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.server.resource.web;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import jakarta.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.http.HttpMethod;
25+
import org.springframework.security.authentication.AuthenticationDetailsSource;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
28+
import org.springframework.security.web.authentication.AuthenticationConverter;
29+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.StringUtils;
32+
33+
/**
34+
* Implementation of {@link AuthenticationConverter}, which converts bearer token to
35+
* {@link BearerTokenAuthenticationToken}
36+
*
37+
* @author Max Batischev
38+
* @since 6.3
39+
*/
40+
public final class BearerTokenAuthenticationConverter
41+
extends AbstractBearerTokenAuthenticationConverter<HttpServletRequest> implements AuthenticationConverter {
42+
43+
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
44+
45+
@Override
46+
public Authentication convert(HttpServletRequest request) {
47+
String token = token(request);
48+
if (StringUtils.hasText(token)) {
49+
BearerTokenAuthenticationToken bearerToken = convertBearerToken(token);
50+
bearerToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
51+
return bearerToken;
52+
}
53+
return null;
54+
}
55+
56+
@Override
57+
protected String resolveAuthorizationHeaderToken(HttpServletRequest request) {
58+
return resolveFromAuthorizationHeader(request.getHeader(this.bearerTokenHeaderName));
59+
}
60+
61+
@Override
62+
protected List<String> resolveParameterTokens(HttpServletRequest request) {
63+
String[] queryParameters = request.getParameterValues("access_token");
64+
if (queryParameters != null) {
65+
return List.of(queryParameters);
66+
}
67+
return Collections.emptyList();
68+
}
69+
70+
@Override
71+
protected HttpMethod getHttpMethod(HttpServletRequest request) {
72+
return HttpMethod.valueOf(request.getMethod());
73+
}
74+
75+
public void setAuthenticationDetailsSource(
76+
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
77+
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
78+
this.authenticationDetailsSource = authenticationDetailsSource;
79+
}
80+
81+
public AuthenticationDetailsSource<HttpServletRequest, ?> getAuthenticationDetailsSource() {
82+
return this.authenticationDetailsSource;
83+
}
84+
85+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,22 +17,14 @@
1717
package org.springframework.security.oauth2.server.resource.web.server.authentication;
1818

1919
import java.util.List;
20-
import java.util.regex.Matcher;
21-
import java.util.regex.Pattern;
2220

2321
import reactor.core.publisher.Mono;
2422

25-
import org.springframework.http.HttpHeaders;
2623
import org.springframework.http.HttpMethod;
2724
import org.springframework.http.server.reactive.ServerHttpRequest;
2825
import org.springframework.security.core.Authentication;
29-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
30-
import org.springframework.security.oauth2.server.resource.BearerTokenError;
31-
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
32-
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
26+
import org.springframework.security.oauth2.server.resource.web.AbstractBearerTokenAuthenticationConverter;
3327
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
34-
import org.springframework.util.CollectionUtils;
35-
import org.springframework.util.StringUtils;
3628
import org.springframework.web.server.ServerWebExchange;
3729

3830
/**
@@ -45,102 +37,31 @@
4537
* @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750
4638
* Section 2: Authenticated Requests</a>
4739
*/
48-
public class ServerBearerTokenAuthenticationConverter implements ServerAuthenticationConverter {
49-
50-
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
51-
Pattern.CASE_INSENSITIVE);
52-
53-
private boolean allowUriQueryParameter = false;
54-
55-
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
40+
public class ServerBearerTokenAuthenticationConverter
41+
extends AbstractBearerTokenAuthenticationConverter<ServerHttpRequest> implements ServerAuthenticationConverter {
5642

5743
@Override
5844
public Mono<Authentication> convert(ServerWebExchange exchange) {
59-
return Mono.fromCallable(() -> token(exchange.getRequest())).map((token) -> {
60-
if (token.isEmpty()) {
61-
BearerTokenError error = invalidTokenError();
62-
throw new OAuth2AuthenticationException(error);
63-
}
64-
return new BearerTokenAuthenticationToken(token);
65-
});
45+
// @formatter:off
46+
return Mono.fromCallable(() -> token(exchange.getRequest()))
47+
.map(this::convertBearerToken);
48+
// @formatter:on
6649
}
6750

68-
private String token(ServerHttpRequest request) {
69-
String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
70-
String parameterToken = resolveAccessTokenFromRequest(request);
71-
72-
if (authorizationHeaderToken != null) {
73-
if (parameterToken != null) {
74-
BearerTokenError error = BearerTokenErrors
75-
.invalidRequest("Found multiple bearer tokens in the request");
76-
throw new OAuth2AuthenticationException(error);
77-
}
78-
return authorizationHeaderToken;
79-
}
80-
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
81-
return parameterToken;
82-
}
83-
return null;
84-
}
85-
86-
private static String resolveAccessTokenFromRequest(ServerHttpRequest request) {
87-
List<String> parameterTokens = request.getQueryParams().get("access_token");
88-
if (CollectionUtils.isEmpty(parameterTokens)) {
89-
return null;
90-
}
91-
if (parameterTokens.size() == 1) {
92-
return parameterTokens.get(0);
93-
}
94-
95-
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
96-
throw new OAuth2AuthenticationException(error);
97-
98-
}
99-
100-
/**
101-
* Set if transport of access token using URI query parameter is supported. Defaults
102-
* to {@code false}.
103-
*
104-
* The spec recommends against using this mechanism for sending bearer tokens, and
105-
* even goes as far as stating that it was only included for completeness.
106-
* @param allowUriQueryParameter if the URI query parameter is supported
107-
*/
108-
public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
109-
this.allowUriQueryParameter = allowUriQueryParameter;
110-
}
111-
112-
/**
113-
* Set this value to configure what header is checked when resolving a Bearer Token.
114-
* This value is defaulted to {@link HttpHeaders#AUTHORIZATION}.
115-
*
116-
* This allows other headers to be used as the Bearer Token source such as
117-
* {@link HttpHeaders#PROXY_AUTHORIZATION}
118-
* @param bearerTokenHeaderName the header to check when retrieving the Bearer Token.
119-
* @since 5.4
120-
*/
121-
public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
122-
this.bearerTokenHeaderName = bearerTokenHeaderName;
123-
}
124-
125-
private String resolveFromAuthorizationHeader(HttpHeaders headers) {
126-
String authorization = headers.getFirst(this.bearerTokenHeaderName);
127-
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
128-
return null;
129-
}
130-
Matcher matcher = authorizationPattern.matcher(authorization);
131-
if (!matcher.matches()) {
132-
BearerTokenError error = invalidTokenError();
133-
throw new OAuth2AuthenticationException(error);
134-
}
135-
return matcher.group("token");
51+
@Override
52+
protected String resolveAuthorizationHeaderToken(ServerHttpRequest request) {
53+
String authorization = request.getHeaders().getFirst(this.bearerTokenHeaderName);
54+
return resolveFromAuthorizationHeader(authorization);
13655
}
13756

138-
private static BearerTokenError invalidTokenError() {
139-
return BearerTokenErrors.invalidToken("Bearer token is malformed");
57+
@Override
58+
protected List<String> resolveParameterTokens(ServerHttpRequest request) {
59+
return request.getQueryParams().get("access_token");
14060
}
14161

142-
private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
143-
return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
62+
@Override
63+
protected HttpMethod getHttpMethod(ServerHttpRequest request) {
64+
return request.getMethod();
14465
}
14566

14667
}

0 commit comments

Comments
 (0)