Skip to content

Commit f415f2a

Browse files
committed
Allow customizing client authentication failures with AuthenticationEntryPoint
Signed-off-by: Joe Grandja <[email protected]>
1 parent 9ecfe49 commit f415f2a

File tree

4 files changed

+288
-68
lines changed

4 files changed

+288
-68
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
19+
import org.springframework.security.oauth2.core.OAuth2Error;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* An {@link OAuth2AuthenticationException} that holds an
24+
* {@link OAuth2ClientAuthenticationToken} and is used by an
25+
* {@code AuthenticationFailureHandler} when handling a failed authentication attempt by
26+
* an OAuth 2.0 Client.
27+
*
28+
* @author Joe Grandja
29+
* @since 1.5
30+
* @see OAuth2ClientAuthenticationToken
31+
*/
32+
public class OAuth2ClientAuthenticationException extends OAuth2AuthenticationException {
33+
34+
private final OAuth2ClientAuthenticationToken clientAuthentication;
35+
36+
/**
37+
* Constructs an {@code OAuth2ClientAuthenticationException} using the provided
38+
* parameters.
39+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
40+
* @param clientAuthentication the {@link OAuth2ClientAuthenticationToken OAuth 2.0
41+
* Client Authentication} request
42+
*/
43+
public OAuth2ClientAuthenticationException(OAuth2Error error,
44+
OAuth2ClientAuthenticationToken clientAuthentication) {
45+
super(error);
46+
Assert.notNull(clientAuthentication, "clientAuthentication cannot be null");
47+
this.clientAuthentication = clientAuthentication;
48+
}
49+
50+
/**
51+
* Constructs an {@code OAuth2ClientAuthenticationException} using the provided
52+
* parameters.
53+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
54+
* @param cause the root cause
55+
* @param clientAuthentication the {@link OAuth2ClientAuthenticationToken OAuth 2.0
56+
* Client Authentication} request
57+
*/
58+
public OAuth2ClientAuthenticationException(OAuth2Error error, Throwable cause,
59+
OAuth2ClientAuthenticationToken clientAuthentication) {
60+
super(error, cause);
61+
Assert.notNull(clientAuthentication, "clientAuthentication cannot be null");
62+
this.clientAuthentication = clientAuthentication;
63+
}
64+
65+
/**
66+
* Returns the {@link OAuth2ClientAuthenticationToken OAuth 2.0 Client Authentication}
67+
* request.
68+
* @return the {@link OAuth2ClientAuthenticationToken}
69+
*/
70+
public OAuth2ClientAuthenticationToken getClientAuthentication() {
71+
return this.clientAuthentication;
72+
}
73+
74+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java

Lines changed: 11 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,32 @@
2424
import jakarta.servlet.http.HttpServletResponse;
2525

2626
import org.springframework.core.log.LogMessage;
27-
import org.springframework.http.HttpStatus;
28-
import org.springframework.http.converter.HttpMessageConverter;
29-
import org.springframework.http.server.ServletServerHttpResponse;
3027
import org.springframework.security.authentication.AbstractAuthenticationToken;
3128
import org.springframework.security.authentication.AuthenticationDetailsSource;
3229
import org.springframework.security.authentication.AuthenticationManager;
3330
import org.springframework.security.core.Authentication;
34-
import org.springframework.security.core.AuthenticationException;
3531
import org.springframework.security.core.context.SecurityContext;
3632
import org.springframework.security.core.context.SecurityContextHolder;
37-
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
3833
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
3934
import org.springframework.security.oauth2.core.OAuth2Error;
4035
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
41-
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
4236
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
4337
import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider;
38+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationException;
4439
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
4540
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
4641
import org.springframework.security.oauth2.server.authorization.authentication.X509ClientCertificateAuthenticationProvider;
4742
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter;
4843
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
4944
import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
45+
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientAuthenticationFailureHandler;
5046
import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
5147
import org.springframework.security.oauth2.server.authorization.web.authentication.X509ClientCertificateAuthenticationConverter;
5248
import org.springframework.security.web.authentication.AuthenticationConverter;
5349
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
5450
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
5551
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
5652
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
57-
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
5853
import org.springframework.security.web.util.matcher.RequestMatcher;
5954
import org.springframework.util.Assert;
6055
import org.springframework.web.filter.OncePerRequestFilter;
@@ -75,6 +70,7 @@
7570
* @see ClientSecretAuthenticationProvider
7671
* @see PublicClientAuthenticationConverter
7772
* @see PublicClientAuthenticationProvider
73+
* @see OAuth2ClientAuthenticationFailureHandler
7874
* @see <a target="_blank" href=
7975
* "https://datatracker.ietf.org/doc/html/rfc6749#section-2.3">Section 2.3 Client
8076
* Authentication</a>
@@ -88,17 +84,13 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
8884

8985
private final RequestMatcher requestMatcher;
9086

91-
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
92-
9387
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
9488

95-
private final BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();
96-
9789
private AuthenticationConverter authenticationConverter;
9890

9991
private AuthenticationSuccessHandler authenticationSuccessHandler = this::onAuthenticationSuccess;
10092

101-
private AuthenticationFailureHandler authenticationFailureHandler = this::onAuthenticationFailure;
93+
private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ClientAuthenticationFailureHandler();
10294

10395
/**
10496
* Constructs an {@code OAuth2ClientAuthenticationFilter} using the provided
@@ -114,7 +106,6 @@ public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationMana
114106
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
115107
this.authenticationManager = authenticationManager;
116108
this.requestMatcher = requestMatcher;
117-
this.basicAuthenticationEntryPoint.setRealmName("default");
118109
// @formatter:off
119110
this.authenticationConverter = new DelegatingAuthenticationConverter(
120111
Arrays.asList(
@@ -138,16 +129,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
138129
Authentication authenticationRequest = null;
139130
try {
140131
authenticationRequest = this.authenticationConverter.convert(request);
132+
if (authenticationRequest == null) {
133+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
134+
}
141135
if (authenticationRequest instanceof AbstractAuthenticationToken authenticationToken) {
142136
authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
143137
}
144-
if (authenticationRequest != null) {
145-
validateClientIdentifier(authenticationRequest);
146-
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
147-
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
148-
}
138+
validateClientIdentifier(authenticationRequest);
139+
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
140+
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
149141
filterChain.doFilter(request, response);
150-
151142
}
152143
catch (OAuth2AuthenticationException ex) {
153144
if (this.logger.isTraceEnabled()) {
@@ -160,8 +151,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
160151
else {
161152
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
162153
}
163-
164154
}
155+
165156
}
166157

167158
/**
@@ -211,35 +202,6 @@ private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResp
211202
}
212203
}
213204

214-
private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
215-
AuthenticationException authenticationException) throws IOException {
216-
217-
SecurityContextHolder.clearContext();
218-
219-
if (authenticationException instanceof OAuth2ClientAuthenticationException clientAuthenticationException) {
220-
OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationException
221-
.getClientAuthentication();
222-
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC
223-
.equals(clientAuthentication.getClientAuthenticationMethod())) {
224-
this.basicAuthenticationEntryPoint.commence(request, response, authenticationException);
225-
return;
226-
}
227-
}
228-
229-
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
230-
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
231-
if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
232-
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
233-
}
234-
else {
235-
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
236-
}
237-
// We don't want to reveal too much information to the caller so just return the
238-
// error code
239-
OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode());
240-
this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
241-
}
242-
243205
private static void validateClientIdentifier(Authentication authentication) {
244206
if (!(authentication instanceof OAuth2ClientAuthenticationToken)) {
245207
return;
@@ -261,21 +223,4 @@ private static void validateClientIdentifier(Authentication authentication) {
261223
}
262224
}
263225

264-
private static final class OAuth2ClientAuthenticationException extends OAuth2AuthenticationException {
265-
266-
private final OAuth2ClientAuthenticationToken clientAuthentication;
267-
268-
private OAuth2ClientAuthenticationException(OAuth2Error error, Throwable cause,
269-
OAuth2ClientAuthenticationToken clientAuthentication) {
270-
super(error, cause);
271-
Assert.notNull(clientAuthentication, "clientAuthentication cannot be null");
272-
this.clientAuthentication = clientAuthentication;
273-
}
274-
275-
private OAuth2ClientAuthenticationToken getClientAuthentication() {
276-
return this.clientAuthentication;
277-
}
278-
279-
}
280-
281226
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.web.authentication;
17+
18+
import java.io.IOException;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import jakarta.servlet.ServletException;
23+
import jakarta.servlet.http.HttpServletRequest;
24+
import jakarta.servlet.http.HttpServletResponse;
25+
26+
import org.springframework.http.HttpStatus;
27+
import org.springframework.http.converter.HttpMessageConverter;
28+
import org.springframework.http.server.ServletServerHttpResponse;
29+
import org.springframework.security.core.AuthenticationException;
30+
import org.springframework.security.core.context.SecurityContextHolder;
31+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
32+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
33+
import org.springframework.security.oauth2.core.OAuth2Error;
34+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
35+
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
36+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationException;
37+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
38+
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
39+
import org.springframework.security.web.AuthenticationEntryPoint;
40+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
41+
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
42+
import org.springframework.util.Assert;
43+
44+
/**
45+
* An implementation of an {@link AuthenticationFailureHandler} used for handling a failed
46+
* authentication attempt by an OAuth 2.0 Client and delegating to an
47+
* {@link AuthenticationEntryPoint} based on the {@link ClientAuthenticationMethod} used
48+
* by the client.
49+
*
50+
* @author Joe Grandja
51+
* @since 1.5
52+
* @see AuthenticationFailureHandler
53+
* @see AuthenticationEntryPoint
54+
* @see OAuth2ClientAuthenticationFilter
55+
* @see OAuth2ClientAuthenticationException
56+
*/
57+
public final class OAuth2ClientAuthenticationFailureHandler implements AuthenticationFailureHandler {
58+
59+
private final Map<ClientAuthenticationMethod, AuthenticationEntryPoint> authenticationEntryPoints;
60+
61+
private AuthenticationEntryPoint defaultAuthenticationEntryPoint = new DefaultAuthenticationEntryPoint();
62+
63+
public OAuth2ClientAuthenticationFailureHandler() {
64+
this.authenticationEntryPoints = new HashMap<>();
65+
BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();
66+
basicAuthenticationEntryPoint.setRealmName("default");
67+
this.authenticationEntryPoints.put(ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
68+
basicAuthenticationEntryPoint);
69+
}
70+
71+
@Override
72+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
73+
AuthenticationException authenticationException) throws IOException, ServletException {
74+
SecurityContextHolder.clearContext();
75+
AuthenticationEntryPoint authenticationEntryPoint = this.defaultAuthenticationEntryPoint;
76+
if (authenticationException instanceof OAuth2ClientAuthenticationException clientAuthenticationException) {
77+
OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationException
78+
.getClientAuthentication();
79+
AuthenticationEntryPoint clientAuthenticationMethodEntryPoint = this.authenticationEntryPoints
80+
.get(clientAuthentication.getClientAuthenticationMethod());
81+
if (clientAuthenticationMethodEntryPoint != null) {
82+
// Override the default
83+
authenticationEntryPoint = clientAuthenticationMethodEntryPoint;
84+
}
85+
}
86+
authenticationEntryPoint.commence(request, response, authenticationException);
87+
}
88+
89+
/**
90+
* Sets the {@link AuthenticationEntryPoint} used for the specified
91+
* {@link ClientAuthenticationMethod}.
92+
* @param authenticationEntryPoint the {@link AuthenticationEntryPoint}
93+
* @param clientAuthenticationMethod the {@link ClientAuthenticationMethod}
94+
*/
95+
public void setAuthenticationEntryPointFor(AuthenticationEntryPoint authenticationEntryPoint,
96+
ClientAuthenticationMethod clientAuthenticationMethod) {
97+
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
98+
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
99+
this.authenticationEntryPoints.put(clientAuthenticationMethod, authenticationEntryPoint);
100+
}
101+
102+
/**
103+
* Sets the default {@link AuthenticationEntryPoint} used when unable to determine the
104+
* {@link ClientAuthenticationMethod} used by the client.
105+
* @param defaultAuthenticationEntryPoint the default {@link AuthenticationEntryPoint}
106+
*/
107+
public void setDefaultAuthenticationEntryPoint(AuthenticationEntryPoint defaultAuthenticationEntryPoint) {
108+
Assert.notNull(defaultAuthenticationEntryPoint, "defaultAuthenticationEntryPoint cannot be null");
109+
this.defaultAuthenticationEntryPoint = defaultAuthenticationEntryPoint;
110+
}
111+
112+
private static final class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {
113+
114+
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
115+
116+
@Override
117+
public void commence(HttpServletRequest request, HttpServletResponse response,
118+
AuthenticationException exception) throws IOException {
119+
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
120+
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
121+
if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
122+
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
123+
}
124+
else {
125+
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
126+
}
127+
// We don't want to reveal too much information to the caller
128+
// so just return the error code
129+
OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode());
130+
this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
131+
}
132+
133+
}
134+
135+
}

0 commit comments

Comments
 (0)