From 25ac17c9c6224e8592eee4a88571b91e7a1ec8c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 28 Nov 2023 13:54:13 -0500 Subject: [PATCH] draft: initial implementation of customization functions in auth error response handler fixes gh-1369 --- .../OidcClientRegistrationEndpointFilter.java | 45 ++++++------- .../oidc/web/OidcUserInfoEndpointFilter.java | 36 +++++------ .../web/OAuth2ClientAuthenticationFilter.java | 64 +++++++++++-------- ...uth2ErrorAuthenticationFailureHandler.java | 16 +++-- 4 files changed, 85 insertions(+), 76 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java index 5de3352ed..7dc845a86 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,10 @@ */ package org.springframework.security.oauth2.server.authorization.oidc.web; -import java.io.IOException; - import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import org.springframework.core.log.LogMessage; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -29,19 +26,18 @@ import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; 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.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken; import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -53,12 +49,16 @@ import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.util.function.BiConsumer; + /** * A {@code Filter} that processes OpenID Connect 1.0 Dynamic Client Registration (and Client Read) Requests. * * @author Ovidiu Popa * @author Joe Grandja * @author Daniel Garnier-Moiroux + * @author Dmitriy Dubson * @since 0.1.1 * @see OidcClientRegistration * @see OidcClientRegistrationAuthenticationConverter @@ -77,11 +77,21 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi private final RequestMatcher clientRegistrationEndpointMatcher; private final HttpMessageConverter clientRegistrationHttpMessageConverter = new OidcClientRegistrationHttpMessageConverter(); - private final HttpMessageConverter errorHttpResponseConverter = - new OAuth2ErrorHttpMessageConverter(); private AuthenticationConverter authenticationConverter = new OidcClientRegistrationAuthenticationConverter(); private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendClientRegistrationResponse; - private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse; + BiConsumer authenticationFailureHttpResponseCustomizer = (e, httpResponse) -> { + HttpStatus httpStatus = HttpStatus.BAD_REQUEST; + if (OAuth2ErrorCodes.INVALID_TOKEN.equals(e.getError().getErrorCode())) { + httpStatus = HttpStatus.UNAUTHORIZED; + } else if (OAuth2ErrorCodes.INSUFFICIENT_SCOPE.equals(e.getError().getErrorCode())) { + httpStatus = HttpStatus.FORBIDDEN; + } else if (OAuth2ErrorCodes.INVALID_CLIENT.equals(e.getError().getErrorCode())) { + httpStatus = HttpStatus.UNAUTHORIZED; + } + httpResponse.setStatusCode(httpStatus); + }; + + private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler(); /** * Constructs an {@code OidcClientRegistrationEndpointFilter} using the provided parameters. @@ -90,6 +100,7 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi */ public OidcClientRegistrationEndpointFilter(AuthenticationManager authenticationManager) { this(authenticationManager, DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI); + ((OAuth2ErrorAuthenticationFailureHandler) authenticationFailureHandler).setHttpResponseCustomizer(authenticationFailureHttpResponseCustomizer); } /** @@ -206,20 +217,4 @@ private void sendClientRegistrationResponse(HttpServletRequest request, HttpServ this.clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpResponse); } - private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authenticationException) throws IOException { - OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError(); - HttpStatus httpStatus = HttpStatus.BAD_REQUEST; - if (OAuth2ErrorCodes.INVALID_TOKEN.equals(error.getErrorCode())) { - httpStatus = HttpStatus.UNAUTHORIZED; - } else if (OAuth2ErrorCodes.INSUFFICIENT_SCOPE.equals(error.getErrorCode())) { - httpStatus = HttpStatus.FORBIDDEN; - } else if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) { - httpStatus = HttpStatus.UNAUTHORIZED; - } - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - httpResponse.setStatusCode(httpStatus); - this.errorHttpResponseConverter.write(error, null, httpResponse); - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilter.java index 610b38710..946253cc2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.authorization.oidc.web; import java.io.IOException; +import java.util.function.BiConsumer; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -29,7 +30,6 @@ import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -39,6 +39,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken; import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcUserInfoHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -54,6 +55,7 @@ * @author Ido Salomon * @author Steve Riesenberg * @author Daniel Garnier-Moiroux + * @author Dmitriy Dubson * @since 0.2.1 * @see OidcUserInfo * @see OidcUserInfoAuthenticationProvider @@ -70,11 +72,20 @@ public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter { private final RequestMatcher userInfoEndpointMatcher; private final HttpMessageConverter userInfoHttpMessageConverter = new OidcUserInfoHttpMessageConverter(); - private final HttpMessageConverter errorHttpResponseConverter = - new OAuth2ErrorHttpMessageConverter(); private AuthenticationConverter authenticationConverter = this::createAuthentication; private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendUserInfoResponse; - private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse; + + BiConsumer authenticationFailureHttpResponseCustomizer = (e, httpResponse) -> { + HttpStatus httpStatus = HttpStatus.BAD_REQUEST; + if (e.getError().getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) { + httpStatus = HttpStatus.UNAUTHORIZED; + } else if (e.getError().getErrorCode().equals(OAuth2ErrorCodes.INSUFFICIENT_SCOPE)) { + httpStatus = HttpStatus.FORBIDDEN; + } + httpResponse.setStatusCode(httpStatus); + }; + + private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler(); /** * Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters. @@ -83,6 +94,7 @@ public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter { */ public OidcUserInfoEndpointFilter(AuthenticationManager authenticationManager) { this(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI); + ((OAuth2ErrorAuthenticationFailureHandler) authenticationFailureHandler).setHttpResponseCustomizer(authenticationFailureHttpResponseCustomizer); } /** @@ -184,18 +196,4 @@ private void sendUserInfoResponse(HttpServletRequest request, HttpServletRespons this.userInfoHttpMessageConverter.write(userInfoAuthenticationToken.getUserInfo(), null, httpResponse); } - private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authenticationException) throws IOException { - OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError(); - HttpStatus httpStatus = HttpStatus.BAD_REQUEST; - if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) { - httpStatus = HttpStatus.UNAUTHORIZED; - } else if (error.getErrorCode().equals(OAuth2ErrorCodes.INSUFFICIENT_SCOPE)) { - httpStatus = HttpStatus.FORBIDDEN; - } - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - httpResponse.setStatusCode(httpStatus); - this.errorHttpResponseConverter.write(error, null, httpResponse); - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java index 6926314d8..33d52af76 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,13 @@ */ package org.springframework.security.oauth2.server.authorization.web; -import java.io.IOException; -import java.util.Arrays; - import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - +import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogMessage; import org.springframework.http.HttpStatus; -import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; @@ -37,6 +33,7 @@ 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.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider; @@ -46,6 +43,7 @@ import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler; import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -55,11 +53,17 @@ import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiConsumer; + /** * A {@code Filter} that processes an authentication request for an OAuth 2.0 Client. * * @author Joe Grandja * @author Patryk Kostrzewa + * @author Dmitriy Dubson * @since 0.0.1 * @see AuthenticationManager * @see JwtClientAssertionAuthenticationConverter @@ -75,13 +79,29 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; private final RequestMatcher requestMatcher; - private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); private AuthenticationConverter authenticationConverter; private AuthenticationSuccessHandler authenticationSuccessHandler = this::onAuthenticationSuccess; private AuthenticationFailureHandler authenticationFailureHandler = this::onAuthenticationFailure; + private final OAuth2ErrorAuthenticationFailureHandler failureHandler = new OAuth2ErrorAuthenticationFailureHandler(); + + BiConsumer authenticationFailureHttpResponseCustomizer = (e, httpResponse) -> { + // TODO + // The authorization server MAY return an HTTP 401 (Unauthorized) status code + // to indicate which HTTP authentication schemes are supported. + // If the client attempted to authenticate via the "Authorization" request header field, + // the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and + // include the "WWW-Authenticate" response header field + // matching the authentication scheme used by the client. + if (OAuth2ErrorCodes.INVALID_CLIENT.equals(e.getError().getErrorCode())) { + httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); + } else { + httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); + } + }; + /** * Constructs an {@code OAuth2ClientAuthenticationFilter} using the provided parameters. * @@ -100,6 +120,13 @@ public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationMana new ClientSecretBasicAuthenticationConverter(), new ClientSecretPostAuthenticationConverter(), new PublicClientAuthenticationConverter())); + + // We don't want to reveal too much information to the caller so just return the error code + Converter> errorParametersConverter = (OAuth2Error error) -> Map.of(OAuth2ParameterNames.ERROR, error.getErrorCode()); + OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter(); + errorHttpMessageConverter.setErrorParametersConverter(errorParametersConverter); + + failureHandler.setHttpResponseCustomizer(authenticationFailureHttpResponseCustomizer); } @Override @@ -178,28 +205,9 @@ private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResp } private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException { - + AuthenticationException exception) throws IOException, ServletException { SecurityContextHolder.clearContext(); - - // TODO - // The authorization server MAY return an HTTP 401 (Unauthorized) status code - // to indicate which HTTP authentication schemes are supported. - // If the client attempted to authenticate via the "Authorization" request header field, - // the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and - // include the "WWW-Authenticate" response header field - // matching the authentication scheme used by the client. - - OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) { - httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); - } else { - httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); - } - // We don't want to reveal too much information to the caller so just return the error code - OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode()); - this.errorHttpResponseConverter.write(errorResponse, null, httpResponse); + failureHandler.onAuthenticationFailure(request, response, exception); } private static void validateClientIdentifier(Authentication authentication) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ErrorAuthenticationFailureHandler.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ErrorAuthenticationFailureHandler.java index a5d350a65..6aeac995e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ErrorAuthenticationFailureHandler.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2ErrorAuthenticationFailureHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.security.oauth2.server.authorization.web.authentication; import java.io.IOException; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -24,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; @@ -46,14 +50,15 @@ public final class OAuth2ErrorAuthenticationFailureHandler implements AuthenticationFailureHandler { private final Log logger = LogFactory.getLog(getClass()); private HttpMessageConverter errorResponseConverter = new OAuth2ErrorHttpMessageConverter(); + private BiConsumer httpResponseCustomizer = (exception, httpResponse) -> httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException { - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); + if (authenticationException instanceof OAuth2AuthenticationException exception) { + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + httpResponseCustomizer.accept(exception, httpResponse); - if (authenticationException instanceof OAuth2AuthenticationException) { OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError(); this.errorResponseConverter.write(error, null, httpResponse); } else { @@ -75,4 +80,7 @@ public void setErrorResponseConverter(HttpMessageConverter errorRes this.errorResponseConverter = errorResponseConverter; } + public void setHttpResponseCustomizer(BiConsumer httpResponseCustomizer) { + this.httpResponseCustomizer = httpResponseCustomizer; + } }