Bläddra i källkod

Add AuthenticationEntryPoint for DPoP

Issue gh-16574

Closes gh-16900
Joe Grandja 4 månader sedan
förälder
incheckning
9c073dbcde

+ 51 - 2
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<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();
+		}
+
+	}
+
 }

+ 10 - 3
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
 	}