Skip to content

Commit 8153fb8

Browse files
committed
Add support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Signed-off-by: Joe Grandja <[email protected]>
1 parent 27cb115 commit 8153fb8

File tree

10 files changed

+1923
-2
lines changed

10 files changed

+1923
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright 2002-2025 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.config.annotation.web.configurers.oauth2.server.resource;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
import jakarta.servlet.http.HttpServletRequest;
25+
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.http.HttpStatus;
28+
import org.springframework.security.authentication.AuthenticationManager;
29+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
30+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
31+
import org.springframework.security.core.Authentication;
32+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
33+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
34+
import org.springframework.security.oauth2.core.OAuth2Error;
35+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
36+
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
37+
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
38+
import org.springframework.security.web.authentication.AuthenticationConverter;
39+
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
40+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
41+
import org.springframework.security.web.authentication.AuthenticationFilter;
42+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
43+
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
44+
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
45+
import org.springframework.security.web.util.matcher.RequestMatcher;
46+
import org.springframework.util.CollectionUtils;
47+
import org.springframework.util.StringUtils;
48+
49+
/**
50+
* @author Joe Grandja
51+
* @since 6.5
52+
* @see DPoPAuthenticationProvider
53+
*/
54+
final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
55+
extends AbstractHttpConfigurer<DPoPAuthenticationConfigurer<B>, B> {
56+
57+
private RequestMatcher requestMatcher;
58+
59+
private AuthenticationConverter authenticationConverter;
60+
61+
private AuthenticationSuccessHandler authenticationSuccessHandler;
62+
63+
private AuthenticationFailureHandler authenticationFailureHandler;
64+
65+
@Override
66+
public void configure(B http) {
67+
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
68+
http.authenticationProvider(new DPoPAuthenticationProvider(authenticationManager));
69+
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
70+
getAuthenticationConverter());
71+
authenticationFilter.setRequestMatcher(getRequestMatcher());
72+
authenticationFilter.setSuccessHandler(getAuthenticationSuccessHandler());
73+
authenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
74+
authenticationFilter.setSecurityContextRepository(new RequestAttributeSecurityContextRepository());
75+
authenticationFilter = postProcess(authenticationFilter);
76+
http.addFilter(authenticationFilter);
77+
}
78+
79+
private RequestMatcher getRequestMatcher() {
80+
if (this.requestMatcher == null) {
81+
this.requestMatcher = new DPoPRequestMatcher();
82+
}
83+
return this.requestMatcher;
84+
}
85+
86+
private AuthenticationConverter getAuthenticationConverter() {
87+
if (this.authenticationConverter == null) {
88+
this.authenticationConverter = new DPoPAuthenticationConverter();
89+
}
90+
return this.authenticationConverter;
91+
}
92+
93+
private AuthenticationSuccessHandler getAuthenticationSuccessHandler() {
94+
if (this.authenticationSuccessHandler == null) {
95+
this.authenticationSuccessHandler = (request, response, authentication) -> {
96+
// No-op - will continue on filter chain
97+
};
98+
}
99+
return this.authenticationSuccessHandler;
100+
}
101+
102+
private AuthenticationFailureHandler getAuthenticationFailureHandler() {
103+
if (this.authenticationFailureHandler == null) {
104+
this.authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(
105+
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
106+
}
107+
return this.authenticationFailureHandler;
108+
}
109+
110+
private static final class DPoPRequestMatcher implements RequestMatcher {
111+
112+
@Override
113+
public boolean matches(HttpServletRequest request) {
114+
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
115+
if (!StringUtils.hasText(authorization)) {
116+
return false;
117+
}
118+
return StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue());
119+
}
120+
121+
}
122+
123+
private static final class DPoPAuthenticationConverter implements AuthenticationConverter {
124+
125+
private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^DPoP (?<token>[a-zA-Z0-9-._~+/]+=*)$",
126+
Pattern.CASE_INSENSITIVE);
127+
128+
@Override
129+
public Authentication convert(HttpServletRequest request) {
130+
List<String> authorizationList = Collections.list(request.getHeaders(HttpHeaders.AUTHORIZATION));
131+
if (CollectionUtils.isEmpty(authorizationList)) {
132+
return null;
133+
}
134+
if (authorizationList.size() != 1) {
135+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
136+
"Found multiple Authorization headers.", null);
137+
throw new OAuth2AuthenticationException(error);
138+
}
139+
String authorization = authorizationList.get(0);
140+
if (!StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue())) {
141+
return null;
142+
}
143+
Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization);
144+
if (!matcher.matches()) {
145+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "DPoP access token is malformed.",
146+
null);
147+
throw new OAuth2AuthenticationException(error);
148+
}
149+
String accessToken = matcher.group("token");
150+
List<String> dPoPProofList = Collections
151+
.list(request.getHeaders(OAuth2AccessToken.TokenType.DPOP.getValue()));
152+
if (CollectionUtils.isEmpty(dPoPProofList) || dPoPProofList.size() != 1) {
153+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
154+
"DPoP proof is missing or invalid.", null);
155+
throw new OAuth2AuthenticationException(error);
156+
}
157+
String dPoPProof = dPoPProofList.get(0);
158+
return new DPoPAuthenticationToken(accessToken, dPoPProof, request.getMethod(),
159+
request.getRequestURL().toString());
160+
}
161+
162+
}
163+
164+
}

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -152,6 +152,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
152152

153153
private final ApplicationContext context;
154154

155+
private final DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
156+
155157
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
156158

157159
private BearerTokenResolver bearerTokenResolver;
@@ -283,6 +285,7 @@ public void configure(H http) {
283285
filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
284286
filter = postProcess(filter);
285287
http.addFilter(filter);
288+
this.dPoPAuthenticationConfigurer.configure(http);
286289
}
287290

288291
private void validateConfiguration() {

0 commit comments

Comments
 (0)