Add AuthenticationEntryPoint for DPoP

Issue gh-16574

Closes gh-16900
This commit is contained in:
Joe Grandja 2025-04-04 17:20:22 -04:00
parent 21a85e3520
commit 9c073dbcde
2 changed files with 61 additions and 5 deletions

View File

@ -17,11 +17,14 @@
package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -29,18 +32,21 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.CollectionUtils;
@ -102,7 +108,7 @@ final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
private AuthenticationFailureHandler getAuthenticationFailureHandler() {
if (this.authenticationFailureHandler == null) {
this.authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
new DPoPAuthenticationEntryPoint());
}
return this.authenticationFailureHandler;
}
@ -161,4 +167,47 @@ final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
}
private static final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) {
Map<String, String> parameters = new LinkedHashMap<>();
if (authenticationException instanceof OAuth2AuthenticationException oauth2AuthenticationException) {
OAuth2Error error = oauth2AuthenticationException.getError();
parameters.put(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (StringUtils.hasText(error.getDescription())) {
parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
}
if (StringUtils.hasText(error.getUri())) {
parameters.put(OAuth2ParameterNames.ERROR_URI, error.getUri());
}
}
parameters.put("algs",
JwsAlgorithms.RS256 + " " + JwsAlgorithms.RS384 + " " + JwsAlgorithms.RS512 + " "
+ JwsAlgorithms.PS256 + " " + JwsAlgorithms.PS384 + " " + JwsAlgorithms.PS512 + " "
+ JwsAlgorithms.ES256 + " " + JwsAlgorithms.ES384 + " " + JwsAlgorithms.ES512);
String wwwAuthenticate = toWWWAuthenticateHeader(parameters);
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
private static String toWWWAuthenticateHeader(Map<String, String> parameters) {
StringBuilder wwwAuthenticate = new StringBuilder();
wwwAuthenticate.append(OAuth2AccessToken.TokenType.DPOP.getValue());
if (!parameters.isEmpty()) {
wwwAuthenticate.append(" ");
int i = 0;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
if (i++ != parameters.size() - 1) {
wwwAuthenticate.append(", ");
}
}
}
return wwwAuthenticate.toString();
}
}
}

View File

@ -70,6 +70,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
@ -120,7 +121,9 @@ public class DPoPAuthenticationConfigurerTests {
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.header("DPoP", dPoPProof))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
"DPoP error=\"invalid_request\", error_description=\"Found multiple Authorization headers.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""));
// @formatter:on
}
@ -134,7 +137,9 @@ public class DPoPAuthenticationConfigurerTests {
this.mvc.perform(get("/resource1")
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken + " m a l f o r m e d ")
.header("DPoP", dPoPProof))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
"DPoP error=\"invalid_token\", error_description=\"DPoP access token is malformed.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""));
// @formatter:on
}
@ -149,7 +154,9 @@ public class DPoPAuthenticationConfigurerTests {
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken)
.header("DPoP", dPoPProof)
.header("DPoP", dPoPProof))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
"DPoP error=\"invalid_request\", error_description=\"DPoP proof is missing or invalid.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""));
// @formatter:on
}