Skip to content

Commit fc6a4c8

Browse files
franticticktickjgrandja
authored andcommitted
Add Support DPoP Customization
Closes gh-16940 Signed-off-by: Max Batischev <mblancer@mail.ru>
1 parent d6b97c7 commit fc6a4c8

11 files changed

Lines changed: 708 additions & 125 deletions

File tree

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

Lines changed: 50 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,24 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource;
1818

19-
import java.util.Collections;
20-
import java.util.LinkedHashMap;
21-
import java.util.List;
22-
import java.util.Map;
23-
import java.util.regex.Matcher;
24-
import java.util.regex.Pattern;
25-
2619
import jakarta.servlet.http.HttpServletRequest;
27-
import jakarta.servlet.http.HttpServletResponse;
2820

29-
import org.springframework.http.HttpHeaders;
30-
import org.springframework.http.HttpStatus;
3121
import org.springframework.security.authentication.AuthenticationManager;
3222
import org.springframework.security.authentication.AuthenticationManagerResolver;
3323
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3424
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
35-
import org.springframework.security.core.Authentication;
36-
import org.springframework.security.core.AuthenticationException;
37-
import org.springframework.security.oauth2.core.OAuth2AccessToken;
38-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
39-
import org.springframework.security.oauth2.core.OAuth2Error;
40-
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
41-
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
42-
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
25+
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationConverter;
4326
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
44-
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
45-
import org.springframework.security.web.AuthenticationEntryPoint;
27+
import org.springframework.security.oauth2.server.resource.web.DPoPAuthenticationEntryPoint;
28+
import org.springframework.security.oauth2.server.resource.web.DPoPRequestMatcher;
4629
import org.springframework.security.web.authentication.AuthenticationConverter;
4730
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
4831
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
4932
import org.springframework.security.web.authentication.AuthenticationFilter;
5033
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
5134
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
5235
import org.springframework.security.web.util.matcher.RequestMatcher;
53-
import org.springframework.util.CollectionUtils;
54-
import org.springframework.util.StringUtils;
36+
import org.springframework.util.Assert;
5537
import org.springframework.web.context.request.RequestAttributes;
5638
import org.springframework.web.context.request.RequestContextHolder;
5739
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -61,12 +43,13 @@
6143
* (DPoP) support.
6244
*
6345
* @author Joe Grandja
46+
* @author Max Batischev
6447
* @since 6.5
6548
* @see DPoPAuthenticationProvider
6649
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
6750
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
6851
*/
69-
final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
52+
public final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
7053
extends AbstractHttpConfigurer<DPoPAuthenticationConfigurer<B>, B> {
7154

7255
private RequestMatcher requestMatcher;
@@ -108,6 +91,50 @@ private AuthenticationManager getTokenAuthenticationManager(B http) {
10891
};
10992
}
11093

94+
/**
95+
* Sets the {@link RequestMatcher} to use.
96+
* @param requestMatcher
97+
* @since 7.0
98+
*/
99+
public DPoPAuthenticationConfigurer<B> requestMatcher(RequestMatcher requestMatcher) {
100+
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
101+
this.requestMatcher = requestMatcher;
102+
return this;
103+
}
104+
105+
/**
106+
* Sets the {@link AuthenticationConverter} to use.
107+
* @param authenticationConverter
108+
* @since 7.0
109+
*/
110+
public DPoPAuthenticationConfigurer<B> authenticationConverter(AuthenticationConverter authenticationConverter) {
111+
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
112+
this.authenticationConverter = authenticationConverter;
113+
return this;
114+
}
115+
116+
/**
117+
* Sets the {@link AuthenticationFailureHandler} to use.
118+
* @param failureHandler
119+
* @since 7.0
120+
*/
121+
public DPoPAuthenticationConfigurer<B> failureHandler(AuthenticationFailureHandler failureHandler) {
122+
Assert.notNull(failureHandler, "failureHandler cannot be null");
123+
this.authenticationFailureHandler = failureHandler;
124+
return this;
125+
}
126+
127+
/**
128+
* Sets the {@link AuthenticationSuccessHandler} to use.
129+
* @param successHandler
130+
* @since 7.0
131+
*/
132+
public DPoPAuthenticationConfigurer<B> successHandler(AuthenticationSuccessHandler successHandler) {
133+
Assert.notNull(successHandler, "successHandler cannot be null");
134+
this.authenticationSuccessHandler = successHandler;
135+
return this;
136+
}
137+
111138
private RequestMatcher getRequestMatcher() {
112139
if (this.requestMatcher == null) {
113140
this.requestMatcher = new DPoPRequestMatcher();
@@ -139,101 +166,4 @@ private AuthenticationFailureHandler getAuthenticationFailureHandler() {
139166
return this.authenticationFailureHandler;
140167
}
141168

142-
private static final class DPoPRequestMatcher implements RequestMatcher {
143-
144-
@Override
145-
public boolean matches(HttpServletRequest request) {
146-
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
147-
if (!StringUtils.hasText(authorization)) {
148-
return false;
149-
}
150-
return StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue());
151-
}
152-
153-
}
154-
155-
private static final class DPoPAuthenticationConverter implements AuthenticationConverter {
156-
157-
private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^DPoP (?<token>[a-zA-Z0-9-._~+/]+=*)$",
158-
Pattern.CASE_INSENSITIVE);
159-
160-
@Override
161-
public Authentication convert(HttpServletRequest request) {
162-
List<String> authorizationList = Collections.list(request.getHeaders(HttpHeaders.AUTHORIZATION));
163-
if (CollectionUtils.isEmpty(authorizationList)) {
164-
return null;
165-
}
166-
if (authorizationList.size() != 1) {
167-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
168-
"Found multiple Authorization headers.", null);
169-
throw new OAuth2AuthenticationException(error);
170-
}
171-
String authorization = authorizationList.get(0);
172-
if (!StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue())) {
173-
return null;
174-
}
175-
Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization);
176-
if (!matcher.matches()) {
177-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "DPoP access token is malformed.",
178-
null);
179-
throw new OAuth2AuthenticationException(error);
180-
}
181-
String accessToken = matcher.group("token");
182-
List<String> dPoPProofList = Collections
183-
.list(request.getHeaders(OAuth2AccessToken.TokenType.DPOP.getValue()));
184-
if (CollectionUtils.isEmpty(dPoPProofList) || dPoPProofList.size() != 1) {
185-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
186-
"DPoP proof is missing or invalid.", null);
187-
throw new OAuth2AuthenticationException(error);
188-
}
189-
String dPoPProof = dPoPProofList.get(0);
190-
return new DPoPAuthenticationToken(accessToken, dPoPProof, request.getMethod(),
191-
request.getRequestURL().toString());
192-
}
193-
194-
}
195-
196-
private static final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint {
197-
198-
@Override
199-
public void commence(HttpServletRequest request, HttpServletResponse response,
200-
AuthenticationException authenticationException) {
201-
Map<String, String> parameters = new LinkedHashMap<>();
202-
if (authenticationException instanceof OAuth2AuthenticationException oauth2AuthenticationException) {
203-
OAuth2Error error = oauth2AuthenticationException.getError();
204-
parameters.put(OAuth2ParameterNames.ERROR, error.getErrorCode());
205-
if (StringUtils.hasText(error.getDescription())) {
206-
parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
207-
}
208-
if (StringUtils.hasText(error.getUri())) {
209-
parameters.put(OAuth2ParameterNames.ERROR_URI, error.getUri());
210-
}
211-
}
212-
parameters.put("algs",
213-
JwsAlgorithms.RS256 + " " + JwsAlgorithms.RS384 + " " + JwsAlgorithms.RS512 + " "
214-
+ JwsAlgorithms.PS256 + " " + JwsAlgorithms.PS384 + " " + JwsAlgorithms.PS512 + " "
215-
+ JwsAlgorithms.ES256 + " " + JwsAlgorithms.ES384 + " " + JwsAlgorithms.ES512);
216-
String wwwAuthenticate = toWWWAuthenticateHeader(parameters);
217-
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
218-
response.setStatus(HttpStatus.UNAUTHORIZED.value());
219-
}
220-
221-
private static String toWWWAuthenticateHeader(Map<String, String> parameters) {
222-
StringBuilder wwwAuthenticate = new StringBuilder();
223-
wwwAuthenticate.append(OAuth2AccessToken.TokenType.DPOP.getValue());
224-
if (!parameters.isEmpty()) {
225-
wwwAuthenticate.append(" ");
226-
int i = 0;
227-
for (Map.Entry<String, String> entry : parameters.entrySet()) {
228-
wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
229-
if (i++ != parameters.size() - 1) {
230-
wwwAuthenticate.append(", ");
231-
}
232-
}
233-
}
234-
return wwwAuthenticate.toString();
235-
}
236-
237-
}
238-
239169
}

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
* @author Josh Cummings
147147
* @author Evgeniy Cheban
148148
* @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
149+
* @author Max Batischev
149150
* @since 5.1
150151
* @see BearerTokenAuthenticationFilter
151152
* @see JwtAuthenticationProvider
@@ -168,6 +169,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
168169

169170
private final ApplicationContext context;
170171

172+
private DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer;
173+
171174
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
172175

173176
private AuthenticationConverter authenticationConverter;
@@ -268,6 +271,22 @@ public OAuth2ResourceServerConfigurer<H> protectedResourceMetadata(
268271
return this;
269272
}
270273

274+
/**
275+
* Enables DPoP support.
276+
* @param dpopAuthenticatioCustomizer the {@link Customizer} to provide more options
277+
* for the {@link DPoPAuthenticationConfigurer}
278+
* @return the {@link OAuth2ResourceServerConfigurer} for further customizations
279+
* @since 7.0
280+
*/
281+
public OAuth2ResourceServerConfigurer<H> dpop(
282+
Customizer<DPoPAuthenticationConfigurer<H>> dpopAuthenticatioCustomizer) {
283+
if (this.dPoPAuthenticationConfigurer == null) {
284+
this.dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
285+
}
286+
dpopAuthenticatioCustomizer.customize(this.dPoPAuthenticationConfigurer);
287+
return this;
288+
}
289+
271290
@Override
272291
public void init(H http) {
273292
validateConfiguration();
@@ -296,9 +315,8 @@ public void configure(H http) {
296315
filter = postProcess(filter);
297316
http.addFilter(filter);
298317

299-
if (dPoPAuthenticationAvailable) {
300-
DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
301-
dPoPAuthenticationConfigurer.configure(http);
318+
if (dPoPAuthenticationAvailable && this.dPoPAuthenticationConfigurer != null) {
319+
this.dPoPAuthenticationConfigurer.configure(http);
302320
}
303321

304322
OAuth2ProtectedResourceMetadataFilter protectedResourceMetadataFilter = new OAuth2ProtectedResourceMetadataFilter();

config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDsl.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolv
2525
import org.springframework.security.web.AuthenticationEntryPoint
2626
import org.springframework.security.web.access.AccessDeniedHandler
2727
import jakarta.servlet.http.HttpServletRequest
28+
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.DPoPAuthenticationConfigurer
29+
import org.springframework.security.config.annotation.web.oauth2.resourceserver.DPoPDsl
2830

2931
/**
3032
* A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 resource server support using
3133
* idiomatic Kotlin code.
3234
*
3335
* @author Eleftheria Stein
36+
* @author Max Batischev
3437
* @since 5.3
3538
* @property accessDeniedHandler the [AccessDeniedHandler] to use for requests authenticating
3639
* with <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s.
@@ -48,6 +51,7 @@ class OAuth2ResourceServerDsl {
4851

4952
private var jwt: ((OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer) -> Unit)? = null
5053
private var opaqueToken: ((OAuth2ResourceServerConfigurer<HttpSecurity>.OpaqueTokenConfigurer) -> Unit)? = null
54+
private var dpop: ((DPoPAuthenticationConfigurer<HttpSecurity>) -> Unit)? = null
5155

5256
/**
5357
* Enables JWT-encoded bearer token support.
@@ -109,6 +113,36 @@ class OAuth2ResourceServerDsl {
109113
this.opaqueToken = OpaqueTokenDsl().apply(opaqueTokenConfig).get()
110114
}
111115

116+
/**
117+
* Enables DPoP support.
118+
*
119+
* Example:
120+
*
121+
* ```
122+
* @Configuration
123+
* @EnableWebSecurity
124+
* class SecurityConfig {
125+
*
126+
* @Bean
127+
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
128+
* http {
129+
* oauth2ResourceServer {
130+
* dpop { }
131+
* }
132+
* }
133+
* return http.build()
134+
* }
135+
* }
136+
* ```
137+
*
138+
* @param dpopConfig custom configurations to configure DPoP support
139+
* @see [DPoPDsl]
140+
* @since 7.0
141+
*/
142+
fun dpop(dpopConfig: DPoPDsl.() -> Unit) {
143+
this.dpop = DPoPDsl().apply(dpopConfig).get()
144+
}
145+
112146
internal fun get(): (OAuth2ResourceServerConfigurer<HttpSecurity>) -> Unit {
113147
return { oauth2ResourceServer ->
114148
accessDeniedHandler?.also { oauth2ResourceServer.accessDeniedHandler(accessDeniedHandler) }
@@ -117,6 +151,7 @@ class OAuth2ResourceServerDsl {
117151
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver) }
118152
jwt?.also { oauth2ResourceServer.jwt(jwt) }
119153
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
154+
dpop?.also { oauth2ResourceServer.dpop(dpop) }
120155
}
121156
}
122157
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.oauth2.resourceserver
18+
19+
import org.springframework.security.config.annotation.web.builders.HttpSecurity
20+
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.DPoPAuthenticationConfigurer
21+
import org.springframework.security.web.authentication.AuthenticationConverter
22+
import org.springframework.security.web.authentication.AuthenticationFailureHandler
23+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
24+
import org.springframework.security.web.util.matcher.RequestMatcher
25+
26+
/**
27+
* A Kotlin DSL to configure DPoP support using idiomatic Kotlin code.
28+
*
29+
* @author Max Batischev
30+
* @property requestMatcher the [RequestMatcher] to use.
31+
* @property authenticationConverter the [AuthenticationConverter] to use.
32+
* @property successHandler the [AuthenticationSuccessHandler] to use.
33+
* @property failureHandler the [AuthenticationFailureHandler] to use.
34+
* @since 7.0
35+
*/
36+
class DPoPDsl {
37+
var requestMatcher: RequestMatcher? = null
38+
var authenticationConverter: AuthenticationConverter? = null
39+
var successHandler: AuthenticationSuccessHandler? = null
40+
var failureHandler: AuthenticationFailureHandler? = null
41+
42+
internal fun get(): (DPoPAuthenticationConfigurer<HttpSecurity>) -> Unit {
43+
return { dpop ->
44+
requestMatcher?.also { dpop.requestMatcher(requestMatcher) }
45+
authenticationConverter?.also { dpop.authenticationConverter(authenticationConverter) }
46+
successHandler?.also { dpop.successHandler(successHandler) }
47+
failureHandler?.also { dpop.failureHandler(failureHandler) }
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)