Skip to content

Commit f1f60c0

Browse files
committed
Improve customizing OIDC UserInfo endpoint
Closes gh-785
1 parent b1b2bc4 commit f1f60c0

File tree

4 files changed

+413
-40
lines changed

4 files changed

+413
-40
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java

+132-16
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,55 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.function.Consumer;
1821
import java.util.function.Function;
1922

2023
import org.springframework.http.HttpMethod;
2124
import org.springframework.security.authentication.AuthenticationManager;
25+
import org.springframework.security.authentication.AuthenticationProvider;
2226
import org.springframework.security.config.annotation.ObjectPostProcessor;
2327
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
28+
import org.springframework.security.core.Authentication;
29+
import org.springframework.security.core.context.SecurityContextHolder;
2430
import org.springframework.security.oauth2.core.OAuth2AccessToken;
31+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
32+
import org.springframework.security.oauth2.core.OAuth2Error;
2533
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
2634
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
2735
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
2836
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
2937
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
3038
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
3139
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
40+
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
3241
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
42+
import org.springframework.security.web.authentication.AuthenticationConverter;
43+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
44+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
3345
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3446
import org.springframework.security.web.util.matcher.OrRequestMatcher;
3547
import org.springframework.security.web.util.matcher.RequestMatcher;
48+
import org.springframework.util.Assert;
3649

3750
/**
3851
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
3952
*
4053
* @author Steve Riesenberg
54+
* @author Daniel Garnier-Moiroux
4155
* @since 0.2.1
4256
* @see OidcConfigurer#userInfoEndpoint
4357
* @see OidcUserInfoEndpointFilter
4458
*/
4559
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
4660
private RequestMatcher requestMatcher;
61+
private final List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
62+
private Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer = (authenticationConverters) -> {};
63+
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
64+
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
65+
private AuthenticationSuccessHandler userInfoResponseHandler;
66+
private AuthenticationFailureHandler errorResponseHandler;
4767
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
4868

4969
/**
@@ -54,22 +74,82 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
5474
}
5575

5676
/**
57-
* Sets the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext}
58-
* to an instance of {@link OidcUserInfo} for the UserInfo response.
77+
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OidcUserInfoAuthenticationToken}.
78+
*
79+
* @param authenticationProvider a {@link AuthenticationProvider} used for authenticating a type of {@link OidcUserInfoAuthenticationToken}
80+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
81+
* @since 0.4.0
82+
*/
83+
public OidcUserInfoEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
84+
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
85+
this.authenticationProviders.add(authenticationProvider);
86+
return this;
87+
}
88+
89+
/**
90+
* Sets the {@code Consumer} providing access to the {@code List} of default
91+
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
92+
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
93+
*
94+
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
95+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
96+
* @since 0.4.0
97+
*/
98+
public OidcUserInfoEndpointConfigurer authenticationProviders(
99+
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
100+
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
101+
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
102+
return this;
103+
}
104+
105+
/**
106+
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken} and
107+
* returning the {@link OidcUserInfo User Info Response}.
108+
*
109+
* @param userInfoResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken}
110+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
111+
* @since 0.4.0
112+
*/
113+
public OidcUserInfoEndpointConfigurer userInfoResponseHandler(AuthenticationSuccessHandler userInfoResponseHandler) {
114+
this.userInfoResponseHandler = userInfoResponseHandler;
115+
return this;
116+
}
117+
118+
/**
119+
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} and
120+
* returning the {@link OAuth2Error Error Response}.
121+
*
122+
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
123+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
124+
* @since 0.4.0
125+
*/
126+
public OidcUserInfoEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
127+
this.errorResponseHandler = errorResponseHandler;
128+
return null;
129+
}
130+
131+
/**
132+
* Sets the {@link Function} used to extract claims from
133+
* {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
134+
* for the UserInfo response.
59135
*
60136
* <p>
61-
* The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the {@link OidcUserInfoAuthenticationToken},
62-
* as well as, the following context attributes:
137+
* The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the
138+
* {@link OidcUserInfoAuthenticationToken}, as well as, the following context
139+
* attributes:
63140
* <ul>
64-
* <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the bearer token used to make the request.</li>
65-
* <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the {@link OidcIdToken} and
66-
* {@link OAuth2AccessToken} associated with the bearer token used to make the request.</li>
141+
* <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the
142+
* bearer token used to make the request.</li>
143+
* <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the
144+
* {@link OidcIdToken} and {@link OAuth2AccessToken} associated with the bearer token
145+
* used to make the request.</li>
67146
* </ul>
68-
*
69-
* @param userInfoMapper the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
147+
* @param userInfoMapper the {@link Function} used to extract claims from
148+
* {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
70149
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
71150
*/
72-
public OidcUserInfoEndpointConfigurer userInfoMapper(Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
151+
public OidcUserInfoEndpointConfigurer userInfoMapper(
152+
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
73153
this.userInfoMapper = userInfoMapper;
74154
return this;
75155
}
@@ -82,13 +162,15 @@ void init(HttpSecurity httpSecurity) {
82162
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
83163
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
84164

85-
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
86-
new OidcUserInfoAuthenticationProvider(
87-
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
88-
if (this.userInfoMapper != null) {
89-
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
165+
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
166+
167+
if (!this.authenticationProviders.isEmpty()) {
168+
authenticationProviders.addAll(0, this.authenticationProviders);
90169
}
91-
httpSecurity.authenticationProvider(postProcess(oidcUserInfoAuthenticationProvider));
170+
this.authenticationProvidersConsumer.accept(authenticationProviders);
171+
172+
authenticationProviders.forEach(authenticationProvider ->
173+
httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
92174
}
93175

94176
@Override
@@ -100,6 +182,15 @@ void configure(HttpSecurity httpSecurity) {
100182
new OidcUserInfoEndpointFilter(
101183
authenticationManager,
102184
authorizationServerSettings.getOidcUserInfoEndpoint());
185+
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
186+
oidcUserInfoEndpointFilter.setAuthenticationConverter(
187+
new DelegatingAuthenticationConverter(authenticationConverters));
188+
if (this.userInfoResponseHandler != null) {
189+
oidcUserInfoEndpointFilter.setAuthenticationSuccessHandler(this.userInfoResponseHandler);
190+
}
191+
if (this.errorResponseHandler != null) {
192+
oidcUserInfoEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
193+
}
103194
httpSecurity.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), FilterSecurityInterceptor.class);
104195
}
105196

@@ -108,4 +199,29 @@ RequestMatcher getRequestMatcher() {
108199
return this.requestMatcher;
109200
}
110201

202+
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
203+
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
204+
authenticationConverters.add(
205+
(request) -> {
206+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
207+
return new OidcUserInfoAuthenticationToken(authentication);
208+
}
209+
);
210+
return authenticationConverters;
211+
}
212+
213+
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
214+
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
215+
216+
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider = new OidcUserInfoAuthenticationProvider(
217+
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
218+
if (this.userInfoMapper != null) {
219+
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
220+
}
221+
authenticationProviders.add(oidcUserInfoAuthenticationProvider);
222+
223+
return authenticationProviders;
224+
}
225+
226+
111227
}

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

+60-10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.http.server.ServletServerHttpResponse;
2929
import org.springframework.security.authentication.AuthenticationManager;
3030
import org.springframework.security.core.Authentication;
31+
import org.springframework.security.core.AuthenticationException;
3132
import org.springframework.security.core.context.SecurityContextHolder;
3233
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
3334
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -36,6 +37,9 @@
3637
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
3738
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
3839
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcUserInfoHttpMessageConverter;
40+
import org.springframework.security.web.authentication.AuthenticationConverter;
41+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
42+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
3943
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4044
import org.springframework.security.web.util.matcher.OrRequestMatcher;
4145
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -61,11 +65,16 @@ public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
6165
private final AuthenticationManager authenticationManager;
6266
private final RequestMatcher userInfoEndpointMatcher;
6367

68+
private AuthenticationConverter authenticationConverter = this::createAuthentication;
69+
6470
private final HttpMessageConverter<OidcUserInfo> userInfoHttpMessageConverter =
6571
new OidcUserInfoHttpMessageConverter();
6672
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
6773
new OAuth2ErrorHttpMessageConverter();
6874

75+
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendUserInfoResponse;
76+
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
77+
6978
/**
7079
* Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters.
7180
*
@@ -100,34 +109,74 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
100109
}
101110

102111
try {
103-
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
104-
105-
OidcUserInfoAuthenticationToken userInfoAuthentication = new OidcUserInfoAuthenticationToken(principal);
112+
Authentication userInfoAuthentication = this.authenticationConverter.convert(request);
106113

107114
OidcUserInfoAuthenticationToken userInfoAuthenticationResult =
108115
(OidcUserInfoAuthenticationToken) this.authenticationManager.authenticate(userInfoAuthentication);
109116

110-
sendUserInfoResponse(response, userInfoAuthenticationResult.getUserInfo());
111-
117+
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, userInfoAuthenticationResult);
112118
} catch (OAuth2AuthenticationException ex) {
113-
sendErrorResponse(response, ex.getError());
119+
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
114120
} catch (Exception ex) {
115121
OAuth2Error error = new OAuth2Error(
116122
OAuth2ErrorCodes.INVALID_REQUEST,
117123
"OpenID Connect 1.0 UserInfo Error: " + ex.getMessage(),
118124
"https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError");
119-
sendErrorResponse(response, error);
125+
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
126+
new OAuth2AuthenticationException(error));
120127
} finally {
121128
SecurityContextHolder.clearContext();
122129
}
123130
}
124131

125-
private void sendUserInfoResponse(HttpServletResponse response, OidcUserInfo userInfo) throws IOException {
132+
/**
133+
* Sets the {@link AuthenticationConverter} used when attempting to extract the OIDC User Info from {@link HttpServletRequest}
134+
* to an instance of {@link OidcUserInfoAuthenticationToken} used for authenticating the User Info request.
135+
*
136+
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an OIDC User Info from {@link HttpServletRequest}
137+
*/
138+
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
139+
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
140+
this.authenticationConverter = authenticationConverter;
141+
}
142+
143+
/**
144+
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken} and
145+
* returning the {@link OidcUserInfo OIDC User Info}.
146+
*
147+
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} for handling an {@link OidcUserInfoAuthenticationToken}
148+
*/
149+
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
150+
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
151+
this.authenticationSuccessHandler = authenticationSuccessHandler;
152+
}
153+
154+
/**
155+
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
156+
* and returning the {@link OAuth2Error Error Response}.
157+
*
158+
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
159+
*/
160+
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
161+
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
162+
this.authenticationFailureHandler = authenticationFailureHandler;
163+
}
164+
165+
private Authentication createAuthentication(HttpServletRequest request) {
166+
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
167+
return new OidcUserInfoAuthenticationToken(principal);
168+
}
169+
170+
private void sendUserInfoResponse(HttpServletRequest request, HttpServletResponse response,
171+
Authentication authentication) throws IOException {
172+
OidcUserInfoAuthenticationToken userInfoAuthenticationToken = (OidcUserInfoAuthenticationToken) authentication;
126173
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
127-
this.userInfoHttpMessageConverter.write(userInfo, null, httpResponse);
174+
this.userInfoHttpMessageConverter.write(userInfoAuthenticationToken.getUserInfo(), null, httpResponse);
128175
}
129176

130-
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
177+
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
178+
AuthenticationException authenticationException) throws IOException {
179+
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
131180
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
132181
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) {
133182
httpStatus = HttpStatus.UNAUTHORIZED;
@@ -138,4 +187,5 @@ private void sendErrorResponse(HttpServletResponse response, OAuth2Error error)
138187
httpResponse.setStatusCode(httpStatus);
139188
this.errorHttpResponseConverter.write(error, null, httpResponse);
140189
}
190+
141191
}

0 commit comments

Comments
 (0)