Skip to content

Commit 9c073db

Browse files
committed
Add AuthenticationEntryPoint for DPoP
Issue gh-16574 Closes gh-16900
1 parent 21a85e3 commit 9c073db

File tree

2 files changed

+61
-5
lines changed

2 files changed

+61
-5
lines changed

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

+51-2
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,36 @@
1717
package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource;
1818

1919
import java.util.Collections;
20+
import java.util.LinkedHashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.regex.Matcher;
2224
import java.util.regex.Pattern;
2325

2426
import jakarta.servlet.http.HttpServletRequest;
27+
import jakarta.servlet.http.HttpServletResponse;
2528

2629
import org.springframework.http.HttpHeaders;
2730
import org.springframework.http.HttpStatus;
2831
import org.springframework.security.authentication.AuthenticationManager;
2932
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3033
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
3134
import org.springframework.security.core.Authentication;
35+
import org.springframework.security.core.AuthenticationException;
3236
import org.springframework.security.oauth2.core.OAuth2AccessToken;
3337
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
3438
import org.springframework.security.oauth2.core.OAuth2Error;
3539
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
40+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
41+
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
3642
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
3743
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
44+
import org.springframework.security.web.AuthenticationEntryPoint;
3845
import org.springframework.security.web.authentication.AuthenticationConverter;
3946
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
4047
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
4148
import org.springframework.security.web.authentication.AuthenticationFilter;
4249
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
43-
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
4450
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
4551
import org.springframework.security.web.util.matcher.RequestMatcher;
4652
import org.springframework.util.CollectionUtils;
@@ -102,7 +108,7 @@ private AuthenticationSuccessHandler getAuthenticationSuccessHandler() {
102108
private AuthenticationFailureHandler getAuthenticationFailureHandler() {
103109
if (this.authenticationFailureHandler == null) {
104110
this.authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(
105-
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
111+
new DPoPAuthenticationEntryPoint());
106112
}
107113
return this.authenticationFailureHandler;
108114
}
@@ -161,4 +167,47 @@ public Authentication convert(HttpServletRequest request) {
161167

162168
}
163169

170+
private static final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint {
171+
172+
@Override
173+
public void commence(HttpServletRequest request, HttpServletResponse response,
174+
AuthenticationException authenticationException) {
175+
Map<String, String> parameters = new LinkedHashMap<>();
176+
if (authenticationException instanceof OAuth2AuthenticationException oauth2AuthenticationException) {
177+
OAuth2Error error = oauth2AuthenticationException.getError();
178+
parameters.put(OAuth2ParameterNames.ERROR, error.getErrorCode());
179+
if (StringUtils.hasText(error.getDescription())) {
180+
parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
181+
}
182+
if (StringUtils.hasText(error.getUri())) {
183+
parameters.put(OAuth2ParameterNames.ERROR_URI, error.getUri());
184+
}
185+
}
186+
parameters.put("algs",
187+
JwsAlgorithms.RS256 + " " + JwsAlgorithms.RS384 + " " + JwsAlgorithms.RS512 + " "
188+
+ JwsAlgorithms.PS256 + " " + JwsAlgorithms.PS384 + " " + JwsAlgorithms.PS512 + " "
189+
+ JwsAlgorithms.ES256 + " " + JwsAlgorithms.ES384 + " " + JwsAlgorithms.ES512);
190+
String wwwAuthenticate = toWWWAuthenticateHeader(parameters);
191+
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
192+
response.setStatus(HttpStatus.UNAUTHORIZED.value());
193+
}
194+
195+
private static String toWWWAuthenticateHeader(Map<String, String> parameters) {
196+
StringBuilder wwwAuthenticate = new StringBuilder();
197+
wwwAuthenticate.append(OAuth2AccessToken.TokenType.DPOP.getValue());
198+
if (!parameters.isEmpty()) {
199+
wwwAuthenticate.append(" ");
200+
int i = 0;
201+
for (Map.Entry<String, String> entry : parameters.entrySet()) {
202+
wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
203+
if (i++ != parameters.size() - 1) {
204+
wwwAuthenticate.append(", ");
205+
}
206+
}
207+
}
208+
return wwwAuthenticate.toString();
209+
}
210+
211+
}
212+
164213
}

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070

7171
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
7272
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
73+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
7374
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
7475

7576
/**
@@ -120,7 +121,9 @@ public void requestWhenDPoPAndBearerAuthenticationThenUnauthorized() throws Exce
120121
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken)
121122
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
122123
.header("DPoP", dPoPProof))
123-
.andExpect(status().isUnauthorized());
124+
.andExpect(status().isUnauthorized())
125+
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
126+
"DPoP error=\"invalid_request\", error_description=\"Found multiple Authorization headers.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""));
124127
// @formatter:on
125128
}
126129

@@ -134,7 +137,9 @@ public void requestWhenDPoPAccessTokenMalformedThenUnauthorized() throws Excepti
134137
this.mvc.perform(get("/resource1")
135138
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken + " m a l f o r m e d ")
136139
.header("DPoP", dPoPProof))
137-
.andExpect(status().isUnauthorized());
140+
.andExpect(status().isUnauthorized())
141+
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
142+
"DPoP error=\"invalid_token\", error_description=\"DPoP access token is malformed.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""));
138143
// @formatter:on
139144
}
140145

@@ -149,7 +154,9 @@ public void requestWhenMultipleDPoPProofsThenUnauthorized() throws Exception {
149154
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken)
150155
.header("DPoP", dPoPProof)
151156
.header("DPoP", dPoPProof))
152-
.andExpect(status().isUnauthorized());
157+
.andExpect(status().isUnauthorized())
158+
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
159+
"DPoP error=\"invalid_request\", error_description=\"DPoP proof is missing or invalid.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""));
153160
// @formatter:on
154161
}
155162

0 commit comments

Comments
 (0)