diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java index acbe822191..b433602e5a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java @@ -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> 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> } + private static final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authenticationException) { + Map 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 parameters) { + StringBuilder wwwAuthenticate = new StringBuilder(); + wwwAuthenticate.append(OAuth2AccessToken.TokenType.DPOP.getValue()); + if (!parameters.isEmpty()) { + wwwAuthenticate.append(" "); + int i = 0; + for (Map.Entry entry : parameters.entrySet()) { + wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\""); + if (i++ != parameters.size() - 1) { + wwwAuthenticate.append(", "); + } + } + } + return wwwAuthenticate.toString(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java index 0011728624..d908607a1f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java @@ -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 }