Skip to content

Commit e91cbc1

Browse files
Max BatischevMax Batischev
Max Batischev
authored and
Max Batischev
committed
Add support BearerTokenAuthenticationConverter
Closes spring-projectsgh-14750
1 parent c8e5fbf commit e91cbc1

File tree

6 files changed

+457
-5
lines changed

6 files changed

+457
-5
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 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.
@@ -41,6 +41,7 @@
4141
import org.springframework.security.oauth2.jwt.Jwt;
4242
import org.springframework.security.oauth2.jwt.JwtDecoder;
4343
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
44+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationConverter;
4445
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
4546
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
4647
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
@@ -56,6 +57,7 @@
5657
import org.springframework.security.web.access.AccessDeniedHandler;
5758
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
5859
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
60+
import org.springframework.security.web.authentication.AuthenticationConverter;
5961
import org.springframework.security.web.csrf.CsrfException;
6062
import org.springframework.security.web.util.matcher.AndRequestMatcher;
6163
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@@ -156,6 +158,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
156158

157159
private BearerTokenResolver bearerTokenResolver;
158160

161+
private AuthenticationConverter authenticationConverter;
162+
159163
private JwtConfigurer jwtConfigurer;
160164

161165
private OpaqueTokenConfigurer opaqueTokenConfigurer;
@@ -198,6 +202,12 @@ public OAuth2ResourceServerConfigurer<H> bearerTokenResolver(BearerTokenResolver
198202
return this;
199203
}
200204

205+
public OAuth2ResourceServerConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
206+
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
207+
this.authenticationConverter = authenticationConverter;
208+
return this;
209+
}
210+
201211
/**
202212
* @deprecated For removal in 7.0. Use {@link #jwt(Customizer)} or
203213
* {@code jwt(Customizer.withDefaults())} to stick with defaults. See the <a href=
@@ -278,6 +288,7 @@ public void configure(H http) {
278288
}
279289

280290
BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver);
291+
filter.setAuthenticationConverter(getAuthenticationConverter());
281292
filter.setBearerTokenResolver(bearerTokenResolver);
282293
filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
283294
filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
@@ -373,6 +384,18 @@ BearerTokenResolver getBearerTokenResolver() {
373384
return this.bearerTokenResolver;
374385
}
375386

387+
AuthenticationConverter getAuthenticationConverter() {
388+
if (this.authenticationConverter == null) {
389+
if (this.context.getBeanNamesForType(AuthenticationConverter.class).length > 0) {
390+
this.authenticationConverter = this.context.getBean(AuthenticationConverter.class);
391+
}
392+
else {
393+
this.authenticationConverter = new BearerTokenAuthenticationConverter();
394+
}
395+
}
396+
return this.authenticationConverter;
397+
}
398+
376399
public class JwtConfigurer {
377400

378401
private final ApplicationContext context;

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

+77-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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.
@@ -117,6 +117,7 @@
117117
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
118118
import org.springframework.security.oauth2.jwt.TestJwts;
119119
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
120+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationConverter;
120121
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
121122
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
122123
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
@@ -133,6 +134,7 @@
133134
import org.springframework.security.web.SecurityFilterChain;
134135
import org.springframework.security.web.access.AccessDeniedHandler;
135136
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
137+
import org.springframework.security.web.authentication.AuthenticationConverter;
136138
import org.springframework.test.web.servlet.MockMvc;
137139
import org.springframework.test.web.servlet.MvcResult;
138140
import org.springframework.test.web.servlet.ResultMatcher;
@@ -766,6 +768,47 @@ public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed()
766768
assertThat(oauth2.getBearerTokenResolver()).isInstanceOf(DefaultBearerTokenResolver.class);
767769
}
768770

771+
@Test
772+
public void getAuthenticationConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() {
773+
AuthenticationConverter converter = mock(AuthenticationConverter.class);
774+
AuthenticationConverter converterBean = mock(AuthenticationConverter.class);
775+
GenericWebApplicationContext context = new GenericWebApplicationContext();
776+
context.registerBean("converterOne", AuthenticationConverter.class, () -> converterBean);
777+
context.registerBean("converterTwo", AuthenticationConverter.class, () -> converterBean);
778+
this.spring.context(context).autowire();
779+
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
780+
oauth2.authenticationConverter(converter);
781+
assertThat(oauth2.getAuthenticationConverter()).isEqualTo(converter);
782+
}
783+
784+
@Test
785+
public void getAuthenticationConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() {
786+
AuthenticationConverter converter = mock(AuthenticationConverter.class);
787+
AuthenticationConverter converterBean = mock(AuthenticationConverter.class);
788+
GenericWebApplicationContext context = new GenericWebApplicationContext();
789+
context.registerBean(AuthenticationConverter.class, () -> converterBean);
790+
this.spring.context(context).autowire();
791+
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
792+
oauth2.authenticationConverter(converter);
793+
assertThat(oauth2.getAuthenticationConverter()).isEqualTo(converter);
794+
}
795+
796+
@Test
797+
public void getAuthenticationConverterWhenDuplicateConverterBeansThenWiringException() {
798+
assertThatExceptionOfType(BeanCreationException.class)
799+
.isThrownBy(
800+
() -> this.spring.register(MultipleAuthenticationConverterBeansConfig.class, JwtDecoderConfig.class)
801+
.autowire())
802+
.withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
803+
}
804+
805+
@Test
806+
public void getAuthenticationConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() {
807+
ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext();
808+
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
809+
assertThat(oauth2.getAuthenticationConverter()).isInstanceOf(BearerTokenAuthenticationConverter.class);
810+
}
811+
769812
@Test
770813
public void requestWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
771814
this.spring.register(CustomAuthenticationDetailsSource.class, JwtDecoderConfig.class, BasicController.class)
@@ -1999,6 +2042,39 @@ BearerTokenResolver allowQueryParameter() {
19992042

20002043
}
20012044

2045+
@Configuration
2046+
@EnableWebSecurity
2047+
static class MultipleAuthenticationConverterBeansConfig {
2048+
2049+
@Bean
2050+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2051+
// @formatter:off
2052+
http
2053+
.authorizeRequests()
2054+
.anyRequest().authenticated()
2055+
.and()
2056+
.oauth2ResourceServer()
2057+
.jwt();
2058+
return http.build();
2059+
// @formatter:on
2060+
}
2061+
2062+
@Bean
2063+
AuthenticationConverter authenticationConverterOne() {
2064+
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
2065+
converter.setAllowUriQueryParameter(true);
2066+
return converter;
2067+
}
2068+
2069+
@Bean
2070+
AuthenticationConverter authenticationConverterTwo() {
2071+
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
2072+
converter.setAllowUriQueryParameter(true);
2073+
return converter;
2074+
}
2075+
2076+
}
2077+
20022078
@Configuration
20032079
@EnableWebSecurity
20042080
static class MultipleBearerTokenResolverBeansConfig {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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.authentication;
18+
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
22+
import jakarta.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.http.HttpMethod;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.security.authentication.AuthenticationDetailsSource;
28+
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.web.authentication.AuthenticationConverter;
33+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
34+
import org.springframework.util.Assert;
35+
import org.springframework.util.StringUtils;
36+
37+
/**
38+
* Implementation of {@link AuthenticationConverter}, that converts bearer token to
39+
* {@link BearerTokenAuthenticationToken}
40+
*
41+
* @author Max Batischev
42+
* @since 6.3
43+
*/
44+
public final class BearerTokenAuthenticationConverter implements AuthenticationConverter {
45+
46+
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
47+
48+
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
49+
Pattern.CASE_INSENSITIVE);
50+
51+
private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
52+
53+
private boolean allowFormEncodedBodyParameter = false;
54+
55+
private boolean allowUriQueryParameter = false;
56+
57+
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
58+
59+
@Override
60+
public Authentication convert(HttpServletRequest request) {
61+
String token = token(request);
62+
if (StringUtils.hasText(token)) {
63+
BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token);
64+
authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
65+
66+
return authenticationToken;
67+
}
68+
return null;
69+
}
70+
71+
private String token(HttpServletRequest request) {
72+
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
73+
final String parameterToken = isParameterTokenSupportedForRequest(request)
74+
? resolveFromRequestParameters(request) : null;
75+
if (authorizationHeaderToken != null) {
76+
if (parameterToken != null) {
77+
final BearerTokenError error = BearerTokenErrors
78+
.invalidRequest("Found multiple bearer tokens in the request");
79+
throw new OAuth2AuthenticationException(error);
80+
}
81+
return authorizationHeaderToken;
82+
}
83+
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
84+
return parameterToken;
85+
}
86+
return null;
87+
}
88+
89+
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
90+
String authorization = request.getHeader(this.bearerTokenHeaderName);
91+
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
92+
return null;
93+
}
94+
Matcher matcher = authorizationPattern.matcher(authorization);
95+
if (!matcher.matches()) {
96+
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
97+
throw new OAuth2AuthenticationException(error);
98+
}
99+
return matcher.group("token");
100+
}
101+
102+
private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) {
103+
return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request)
104+
&& !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request)));
105+
}
106+
107+
private static String resolveFromRequestParameters(HttpServletRequest request) {
108+
String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME);
109+
if (values == null || values.length == 0) {
110+
return null;
111+
}
112+
if (values.length == 1) {
113+
return values[0];
114+
}
115+
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
116+
throw new OAuth2AuthenticationException(error);
117+
}
118+
119+
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
120+
return isFormEncodedRequest(request) || isGetRequest(request);
121+
}
122+
123+
private boolean isGetRequest(HttpServletRequest request) {
124+
return HttpMethod.GET.name().equals(request.getMethod());
125+
}
126+
127+
private boolean isFormEncodedRequest(HttpServletRequest request) {
128+
return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType());
129+
}
130+
131+
private static boolean hasAccessTokenInQueryString(HttpServletRequest request) {
132+
return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME);
133+
}
134+
135+
/**
136+
* Set if transport of access token using URI query parameter is supported. Defaults
137+
* to {@code false}.
138+
*
139+
* The spec recommends against using this mechanism for sending bearer tokens, and
140+
* even goes as far as stating that it was only included for completeness.
141+
* @param allowUriQueryParameter if the URI query parameter is supported
142+
*/
143+
public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
144+
this.allowUriQueryParameter = allowUriQueryParameter;
145+
}
146+
147+
/**
148+
* Set this value to configure what header is checked when resolving a Bearer Token.
149+
* This value is defaulted to {@link HttpHeaders#AUTHORIZATION}.
150+
*
151+
* This allows other headers to be used as the Bearer Token source such as
152+
* {@link HttpHeaders#PROXY_AUTHORIZATION}
153+
* @param bearerTokenHeaderName the header to check when retrieving the Bearer Token.
154+
*/
155+
public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
156+
this.bearerTokenHeaderName = bearerTokenHeaderName;
157+
}
158+
159+
/**
160+
* Set if transport of access token using form-encoded body parameter is supported.
161+
* Defaults to {@code false}.
162+
* @param allowFormEncodedBodyParameter if the form-encoded body parameter is
163+
* supported
164+
*/
165+
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
166+
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
167+
}
168+
169+
/**
170+
* Set the {@link AuthenticationDetailsSource} to use. Defaults to
171+
* {@link WebAuthenticationDetailsSource}.
172+
* @param authenticationDetailsSource the {@code AuthenticationDetailsSource} to use
173+
*/
174+
public void setAuthenticationDetailsSource(
175+
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
176+
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
177+
this.authenticationDetailsSource = authenticationDetailsSource;
178+
}
179+
180+
}

0 commit comments

Comments
 (0)