Skip to content

Commit 6c7b011

Browse files
committed
Add configuration options to OidcUserInfoEndpointConfigurer
- Issue gh-785
1 parent 42e8fb0 commit 6c7b011

File tree

2 files changed

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

2 files changed

+204
-7
lines changed

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

+102-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
165+
}
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,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,13 @@ 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+
return authenticationProviders;
206+
}
207+
111208
}

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,10 +34,13 @@
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;
42+
import org.springframework.http.HttpStatus;
43+
import org.springframework.security.authentication.AuthenticationProvider;
3744
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3845
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3946
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
@@ -60,9 +67,14 @@
6067
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
6168
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
6269
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
70+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
71+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
6372
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
6473
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
74+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
6575
import org.springframework.security.web.SecurityFilterChain;
76+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
77+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
6678
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
6779
import org.springframework.security.web.context.SecurityContextRepository;
6880
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -72,10 +84,13 @@
7284

7385
import static org.assertj.core.api.Assertions.assertThat;
7486
import static org.mockito.ArgumentMatchers.any;
87+
import static org.mockito.ArgumentMatchers.eq;
88+
import static org.mockito.Mockito.doAnswer;
7589
import static org.mockito.Mockito.mock;
7690
import static org.mockito.Mockito.reset;
7791
import static org.mockito.Mockito.spy;
7892
import static org.mockito.Mockito.verify;
93+
import static org.mockito.Mockito.verifyNoInteractions;
7994
import static org.mockito.Mockito.when;
8095
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8196
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -100,21 +115,40 @@ public class OidcUserInfoTests {
100115
@Autowired
101116
private JwtEncoder jwtEncoder;
102117

118+
@Autowired
119+
private JwtDecoder jwtDecoder;
120+
103121
@Autowired
104122
private OAuth2AuthorizationService authorizationService;
105123

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

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

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

120154
@Test
@@ -155,16 +189,78 @@ public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
155189

156190
OAuth2Authorization authorization = createAuthorization();
157191
this.authorizationService.save(authorization);
192+
158193
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
159194

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

170266
// gh-482
@@ -290,6 +386,10 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
290386
.oidc(oidc -> oidc
291387
.userInfoEndpoint(userInfo -> userInfo
292388
.userInfoMapper(userInfoMapper)
389+
.authenticationProvider(authenticationProvider)
390+
.authenticationProviders(authenticationProvidersConsumer)
391+
.userInfoResponseHandler(authenticationSuccessHandler)
392+
.errorResponseHandler(authenticationFailureHandler)
293393
)
294394
);
295395
// @formatter:on

0 commit comments

Comments
 (0)