Skip to content

Commit 129ff3e

Browse files
committed
Add configuration options to OidcUserInfoEndpointConfigurer
- Issue gh-785
1 parent 9f97df6 commit 129ff3e

File tree

2 files changed

+205
-7
lines changed
  • oauth2-authorization-server/src

2 files changed

+205
-7
lines changed

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

+103-5
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
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;
2428
import org.springframework.security.oauth2.core.OAuth2AccessToken;
29+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
30+
import org.springframework.security.oauth2.core.OAuth2Error;
2531
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
2632
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
2733
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
@@ -30,21 +36,29 @@
3036
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
3137
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
3238
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
39+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
40+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
3341
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3442
import org.springframework.security.web.util.matcher.OrRequestMatcher;
3543
import org.springframework.security.web.util.matcher.RequestMatcher;
44+
import org.springframework.util.Assert;
3645

3746
/**
3847
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
3948
*
4049
* @author Steve Riesenberg
50+
* @author Daniel Garnier-Moiroux
4151
* @since 0.2.1
4252
* @see OidcConfigurer#userInfoEndpoint
4353
* @see OidcUserInfoEndpointFilter
4454
*/
4555
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
4656
private RequestMatcher requestMatcher;
4757
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
58+
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
59+
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
60+
private AuthenticationSuccessHandler userInfoResponseHandler;
61+
private AuthenticationFailureHandler errorResponseHandler;
4862

4963
/**
5064
* Restrict for internal use only.
@@ -74,6 +88,63 @@ public OidcUserInfoEndpointConfigurer userInfoMapper(Function<OidcUserInfoAuthen
7488
return this;
7589
}
7690

91+
/**
92+
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OidcUserInfoAuthenticationToken}.
93+
*
94+
* @param authenticationProvider a {@link AuthenticationProvider} used for authenticating a type of {@link OidcUserInfoAuthenticationToken}
95+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
96+
* @since 0.4.0
97+
*/
98+
public OidcUserInfoEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
99+
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
100+
this.authenticationProviders.add(authenticationProvider);
101+
return this;
102+
}
103+
104+
/**
105+
* Sets the {@code Consumer} providing access to the {@code List} of default
106+
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
107+
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
108+
*
109+
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
110+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
111+
* @since 0.4.0
112+
*/
113+
public OidcUserInfoEndpointConfigurer authenticationProviders(
114+
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
115+
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
116+
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
117+
return this;
118+
}
119+
120+
/**
121+
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken} and
122+
* returning the {@link OidcUserInfo User Info Response}.
123+
*
124+
* @param userInfoResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken}
125+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
126+
* @since 0.4.0
127+
*/
128+
public OidcUserInfoEndpointConfigurer userInfoResponseHandler(AuthenticationSuccessHandler userInfoResponseHandler) {
129+
Assert.notNull(userInfoResponseHandler, "userInfoResponseHandler cannot be null");
130+
this.userInfoResponseHandler = userInfoResponseHandler;
131+
return this;
132+
}
133+
134+
/**
135+
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} and
136+
* returning the {@link OAuth2Error Error Response}.
137+
*
138+
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
139+
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
140+
* @since 0.4.0
141+
*/
142+
public OidcUserInfoEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
143+
Assert.notNull(errorResponseHandler, "errorResponseHandler cannot be null");
144+
this.errorResponseHandler = errorResponseHandler;
145+
return null;
146+
}
147+
77148
@Override
78149
void init(HttpSecurity httpSecurity) {
79150
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
@@ -82,13 +153,24 @@ void init(HttpSecurity httpSecurity) {
82153
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
83154
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
84155

85-
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
86-
new OidcUserInfoAuthenticationProvider(
87-
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
156+
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
157+
88158
if (this.userInfoMapper != null) {
89-
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
159+
// @formatter:off
160+
authenticationProviders.stream()
161+
.filter(OidcUserInfoAuthenticationProvider.class::isInstance)
162+
.map(OidcUserInfoAuthenticationProvider.class::cast)
163+
.forEach(provider -> provider.setUserInfoMapper(this.userInfoMapper));
164+
// @formatter:on
90165
}
91-
httpSecurity.authenticationProvider(postProcess(oidcUserInfoAuthenticationProvider));
166+
167+
if (!this.authenticationProviders.isEmpty()) {
168+
authenticationProviders.addAll(0, this.authenticationProviders);
169+
}
170+
this.authenticationProvidersConsumer.accept(authenticationProviders);
171+
172+
authenticationProviders.forEach(authenticationProvider ->
173+
httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
92174
}
93175

94176
@Override
@@ -100,6 +182,12 @@ void configure(HttpSecurity httpSecurity) {
100182
new OidcUserInfoEndpointFilter(
101183
authenticationManager,
102184
authorizationServerSettings.getOidcUserInfoEndpoint());
185+
if (this.userInfoResponseHandler != null) {
186+
oidcUserInfoEndpointFilter.setAuthenticationSuccessHandler(this.userInfoResponseHandler);
187+
}
188+
if (this.errorResponseHandler != null) {
189+
oidcUserInfoEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
190+
}
103191
httpSecurity.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), FilterSecurityInterceptor.class);
104192
}
105193

@@ -108,4 +196,14 @@ RequestMatcher getRequestMatcher() {
108196
return this.requestMatcher;
109197
}
110198

199+
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
200+
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
201+
202+
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider = new OidcUserInfoAuthenticationProvider(
203+
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
204+
authenticationProviders.add(oidcUserInfoAuthenticationProvider);
205+
206+
return authenticationProviders;
207+
}
208+
111209
}

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java

+102-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919
import java.util.Arrays;
2020
import java.util.Collections;
2121
import java.util.HashSet;
22+
import java.util.List;
2223
import java.util.Set;
24+
import java.util.function.Consumer;
2325
import java.util.function.Function;
2426

27+
import javax.servlet.http.HttpServletResponse;
28+
2529
import com.nimbusds.jose.jwk.JWKSet;
2630
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
2731
import com.nimbusds.jose.jwk.source.JWKSource;
@@ -30,11 +34,14 @@
3034
import org.junit.BeforeClass;
3135
import org.junit.Rule;
3236
import org.junit.Test;
37+
import org.mockito.ArgumentCaptor;
3338

3439
import org.springframework.beans.factory.annotation.Autowired;
3540
import org.springframework.context.annotation.Bean;
3641
import org.springframework.http.HttpHeaders;
3742
import org.springframework.security.config.Customizer;
43+
import org.springframework.http.HttpStatus;
44+
import org.springframework.security.authentication.AuthenticationProvider;
3845
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3946
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
4047
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
@@ -61,9 +68,14 @@
6168
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
6269
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
6370
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
71+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
72+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
6473
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
6574
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
75+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
6676
import org.springframework.security.web.SecurityFilterChain;
77+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
78+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
6779
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
6880
import org.springframework.security.web.context.SecurityContextRepository;
6981
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -73,10 +85,13 @@
7385

7486
import static org.assertj.core.api.Assertions.assertThat;
7587
import static org.mockito.ArgumentMatchers.any;
88+
import static org.mockito.ArgumentMatchers.eq;
89+
import static org.mockito.Mockito.doAnswer;
7690
import static org.mockito.Mockito.mock;
7791
import static org.mockito.Mockito.reset;
7892
import static org.mockito.Mockito.spy;
7993
import static org.mockito.Mockito.verify;
94+
import static org.mockito.Mockito.verifyNoInteractions;
8095
import static org.mockito.Mockito.when;
8196
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8297
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -101,21 +116,40 @@ public class OidcUserInfoTests {
101116
@Autowired
102117
private JwtEncoder jwtEncoder;
103118

119+
@Autowired
120+
private JwtDecoder jwtDecoder;
121+
104122
@Autowired
105123
private OAuth2AuthorizationService authorizationService;
106124

107125
private static Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
108126

127+
private static AuthenticationProvider authenticationProvider;
128+
129+
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
130+
131+
private static AuthenticationSuccessHandler authenticationSuccessHandler;
132+
133+
private static AuthenticationFailureHandler authenticationFailureHandler;
134+
109135
@BeforeClass
110136
public static void init() {
111137
securityContextRepository = spy(new HttpSessionSecurityContextRepository());
112138
userInfoMapper = mock(Function.class);
139+
authenticationProvider = mock(AuthenticationProvider.class);
140+
authenticationProvidersConsumer = mock(Consumer.class);
141+
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
142+
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
113143
}
114144

115145
@Before
116146
public void setup() {
117147
reset(securityContextRepository);
118148
reset(userInfoMapper);
149+
reset(authenticationProvider);
150+
reset(authenticationProvidersConsumer);
151+
reset(authenticationSuccessHandler);
152+
reset(authenticationFailureHandler);
119153
}
120154

121155
@Test
@@ -156,16 +190,78 @@ public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
156190

157191
OAuth2Authorization authorization = createAuthorization();
158192
this.authorizationService.save(authorization);
193+
159194
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
160195

161196
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
162197
// @formatter:off
163198
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
164199
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
165-
.andExpect(status().is2xxSuccessful())
166-
.andExpectAll(userInfoResponse());
200+
.andExpect(status().is2xxSuccessful());
167201
// @formatter:on
168202
verify(userInfoMapper).apply(any());
203+
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
204+
verifyNoInteractions(authenticationFailureHandler);
205+
206+
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
207+
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
208+
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
209+
assertThat(authenticationProviders).hasSize(2).allMatch(provider ->
210+
provider == authenticationProvider ||
211+
provider instanceof OidcUserInfoAuthenticationProvider
212+
);
213+
}
214+
215+
@Test
216+
public void requestWhenUserInfoEndpointCustomizedThenAuthenticationProviderUsed() throws Exception {
217+
this.spring.register(CustomUserInfoConfiguration.class).autowire();
218+
219+
OAuth2Authorization authorization = createAuthorization();
220+
this.authorizationService.save(authorization);
221+
222+
when(authenticationProvider.supports(eq(OidcUserInfoAuthenticationToken.class))).thenReturn(true);
223+
String tokenValue = authorization.getAccessToken().getToken().getTokenValue();
224+
Jwt jwt = this.jwtDecoder.decode(tokenValue);
225+
OidcUserInfoAuthenticationToken oidcUserInfoAuthentication = new OidcUserInfoAuthenticationToken(
226+
new JwtAuthenticationToken(jwt), createUserInfo());
227+
when(authenticationProvider.authenticate(any())).thenReturn(oidcUserInfoAuthentication);
228+
229+
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
230+
// @formatter:off
231+
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
232+
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
233+
.andExpect(status().is2xxSuccessful());
234+
// @formatter:on
235+
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
236+
verify(authenticationProvider).authenticate(any());
237+
verifyNoInteractions(authenticationFailureHandler);
238+
verifyNoInteractions(userInfoMapper);
239+
}
240+
241+
@Test
242+
public void requestWhenUserInfoEndpointCustomizedAndErrorThenUsed() throws Exception {
243+
this.spring.register(CustomUserInfoConfiguration.class).autowire();
244+
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
245+
doAnswer(
246+
invocation -> {
247+
HttpServletResponse response = invocation.getArgument(1);
248+
response.setStatus(HttpStatus.UNAUTHORIZED.value());
249+
response.getWriter().write("unauthorized");
250+
return null;
251+
}
252+
).when(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
253+
254+
OAuth2AccessToken accessToken = createAuthorization().getAccessToken().getToken();
255+
256+
257+
// @formatter:off
258+
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
259+
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
260+
.andExpect(status().is4xxClientError());
261+
// @formatter:on
262+
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
263+
verifyNoInteractions(authenticationSuccessHandler);
264+
verifyNoInteractions(userInfoMapper);
169265
}
170266

171267
// gh-482
@@ -291,6 +387,10 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
291387
.oidc(oidc -> oidc
292388
.userInfoEndpoint(userInfo -> userInfo
293389
.userInfoMapper(userInfoMapper)
390+
.authenticationProvider(authenticationProvider)
391+
.authenticationProviders(authenticationProvidersConsumer)
392+
.userInfoResponseHandler(authenticationSuccessHandler)
393+
.errorResponseHandler(authenticationFailureHandler)
294394
)
295395
);
296396
// @formatter:on

0 commit comments

Comments
 (0)