Skip to content

Commit a08c12b

Browse files
committed
Support configurable JwtDecoder for IdToken verification
Fixes gh-5717
1 parent 3bcb1d9 commit a08c12b

File tree

12 files changed

+445
-130
lines changed

12 files changed

+445
-130
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

+19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
1717

1818
import org.springframework.beans.factory.BeanFactoryUtils;
19+
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
1920
import org.springframework.context.ApplicationContext;
2021
import org.springframework.core.ResolvableType;
2122
import org.springframework.security.authentication.AuthenticationProvider;
@@ -55,6 +56,7 @@
5556
import org.springframework.security.oauth2.core.oidc.OidcScopes;
5657
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
5758
import org.springframework.security.oauth2.core.user.OAuth2User;
59+
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
5860
import org.springframework.security.web.AuthenticationEntryPoint;
5961
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
6062
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
@@ -488,6 +490,10 @@ public void init(B http) throws Exception {
488490

489491
OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider =
490492
new OidcAuthorizationCodeAuthenticationProvider(accessTokenResponseClient, oidcUserService);
493+
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = this.getJwtDecoderFactoryBean();
494+
if (jwtDecoderFactory != null) {
495+
oidcAuthorizationCodeAuthenticationProvider.setJwtDecoderFactory(jwtDecoderFactory);
496+
}
491497
if (userAuthoritiesMapper != null) {
492498
oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
493499
}
@@ -541,6 +547,19 @@ protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingU
541547
return new AntPathRequestMatcher(loginProcessingUrl);
542548
}
543549

550+
@SuppressWarnings("unchecked")
551+
private JwtDecoderFactory<ClientRegistration> getJwtDecoderFactoryBean() {
552+
ResolvableType type = ResolvableType.forClassWithGenerics(JwtDecoderFactory.class, ClientRegistration.class);
553+
String[] names = this.getBuilder().getSharedObject(ApplicationContext.class).getBeanNamesForType(type);
554+
if (names.length > 1) {
555+
throw new NoUniqueBeanDefinitionException(type, names);
556+
}
557+
if (names.length == 1) {
558+
return (JwtDecoderFactory<ClientRegistration>) this.getBuilder().getSharedObject(ApplicationContext.class).getBean(names[0]);
559+
}
560+
return null;
561+
}
562+
544563
private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() {
545564
GrantedAuthoritiesMapper grantedAuthoritiesMapper =
546565
this.getBuilder().getSharedObject(GrantedAuthoritiesMapper.class);

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+33-26
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,6 @@
1616

1717
package org.springframework.security.config.web.server;
1818

19-
import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
20-
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match;
21-
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch;
22-
23-
import java.io.IOException;
24-
import java.io.PrintWriter;
25-
import java.io.StringWriter;
26-
import java.security.interfaces.RSAPublicKey;
27-
import java.time.Duration;
28-
import java.util.ArrayList;
29-
import java.util.Arrays;
30-
import java.util.Collections;
31-
import java.util.HashMap;
32-
import java.util.List;
33-
import java.util.Map;
34-
import java.util.Optional;
35-
import java.util.function.Function;
36-
import java.util.UUID;
37-
38-
import reactor.core.publisher.Mono;
39-
import reactor.util.context.Context;
40-
4119
import org.springframework.beans.BeansException;
4220
import org.springframework.context.ApplicationContext;
4321
import org.springframework.core.Ordered;
@@ -55,6 +33,8 @@
5533
import org.springframework.security.authorization.ReactiveAuthorizationManager;
5634
import org.springframework.security.core.Authentication;
5735
import org.springframework.security.core.AuthenticationException;
36+
import org.springframework.security.core.GrantedAuthority;
37+
import org.springframework.security.core.authority.AuthorityUtils;
5838
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
5939
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
6040
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
@@ -80,6 +60,7 @@
8060
import org.springframework.security.oauth2.jwt.Jwt;
8161
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
8262
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
63+
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
8364
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
8465
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
8566
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
@@ -92,6 +73,7 @@
9273
import org.springframework.security.web.server.SecurityWebFilterChain;
9374
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
9475
import org.springframework.security.web.server.WebFilterExchange;
76+
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
9577
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
9678
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
9779
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
@@ -159,9 +141,27 @@
159141
import org.springframework.web.server.ServerWebExchange;
160142
import org.springframework.web.server.WebFilter;
161143
import org.springframework.web.server.WebFilterChain;
162-
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
163-
import org.springframework.security.core.GrantedAuthority;
164-
import org.springframework.security.core.authority.AuthorityUtils;
144+
import reactor.core.publisher.Mono;
145+
import reactor.util.context.Context;
146+
147+
import java.io.IOException;
148+
import java.io.PrintWriter;
149+
import java.io.StringWriter;
150+
import java.security.interfaces.RSAPublicKey;
151+
import java.time.Duration;
152+
import java.util.ArrayList;
153+
import java.util.Arrays;
154+
import java.util.Collections;
155+
import java.util.HashMap;
156+
import java.util.List;
157+
import java.util.Map;
158+
import java.util.Optional;
159+
import java.util.UUID;
160+
import java.util.function.Function;
161+
162+
import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
163+
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match;
164+
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch;
165165

166166
/**
167167
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux.
@@ -618,7 +618,14 @@ private ReactiveAuthenticationManager createDefault() {
618618
boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent(
619619
"org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader());
620620
if (oidcAuthenticationProviderEnabled) {
621-
OidcAuthorizationCodeReactiveAuthenticationManager oidc = new OidcAuthorizationCodeReactiveAuthenticationManager(client, getOidcUserService());
621+
OidcAuthorizationCodeReactiveAuthenticationManager oidc =
622+
new OidcAuthorizationCodeReactiveAuthenticationManager(client, getOidcUserService());
623+
ResolvableType type = ResolvableType.forClassWithGenerics(
624+
ReactiveJwtDecoderFactory.class, ClientRegistration.class);
625+
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = getBeanOrNull(type);
626+
if (jwtDecoderFactory != null) {
627+
oidc.setJwtDecoderFactory(jwtDecoderFactory);
628+
}
622629
result = new DelegatingReactiveAuthenticationManager(oidc, result);
623630
}
624631
return result;

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java

+52-27
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
import org.junit.After;
2020
import org.junit.Before;
2121
import org.junit.Test;
22-
import org.springframework.beans.PropertyAccessorFactory;
22+
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
2323
import org.springframework.beans.factory.annotation.Autowired;
2424
import org.springframework.context.ApplicationListener;
2525
import org.springframework.context.ConfigurableApplicationContext;
2626
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
2728
import org.springframework.http.MediaType;
2829
import org.springframework.mock.web.MockFilterChain;
2930
import org.springframework.mock.web.MockHttpServletRequest;
@@ -50,7 +51,6 @@
5051
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
5152
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
5253
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
53-
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
5454
import org.springframework.security.oauth2.core.OAuth2AccessToken;
5555
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
5656
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
@@ -66,6 +66,7 @@
6666
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
6767
import org.springframework.security.oauth2.jwt.Jwt;
6868
import org.springframework.security.oauth2.jwt.JwtDecoder;
69+
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
6970
import org.springframework.security.web.FilterChainProxy;
7071
import org.springframework.security.web.context.HttpRequestResponseHolder;
7172
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@@ -81,6 +82,7 @@
8182
import java.util.Map;
8283

8384
import static org.assertj.core.api.Assertions.assertThat;
85+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
8486
import static org.mockito.ArgumentMatchers.any;
8587
import static org.mockito.Mockito.mock;
8688
import static org.mockito.Mockito.when;
@@ -369,8 +371,7 @@ public void oauth2LoginWithCustomLoginPageThenRedirectCustomLoginPage() throws E
369371
@Test
370372
public void oidcLogin() throws Exception {
371373
// setup application context
372-
loadConfig(OAuth2LoginConfig.class);
373-
registerJwtDecoder();
374+
loadConfig(OAuth2LoginConfig.class, JwtDecoderFactoryConfig.class);
374375

375376
// setup authorization request
376377
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
@@ -396,8 +397,7 @@ public void oidcLogin() throws Exception {
396397
@Test
397398
public void oidcLoginCustomWithConfigurer() throws Exception {
398399
// setup application context
399-
loadConfig(OAuth2LoginConfigCustomWithConfigurer.class);
400-
registerJwtDecoder();
400+
loadConfig(OAuth2LoginConfigCustomWithConfigurer.class, JwtDecoderFactoryConfig.class);
401401

402402
// setup authorization request
403403
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
@@ -423,8 +423,7 @@ public void oidcLoginCustomWithConfigurer() throws Exception {
423423
@Test
424424
public void oidcLoginCustomWithBeanRegistration() throws Exception {
425425
// setup application context
426-
loadConfig(OAuth2LoginConfigCustomWithBeanRegistration.class);
427-
registerJwtDecoder();
426+
loadConfig(OAuth2LoginConfigCustomWithBeanRegistration.class, JwtDecoderFactoryConfig.class);
428427

429428
// setup authorization request
430429
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
@@ -447,6 +446,15 @@ public void oidcLoginCustomWithBeanRegistration() throws Exception {
447446
assertThat(authentication.getAuthorities()).last().hasToString("ROLE_OIDC_USER");
448447
}
449448

449+
@Test
450+
public void oidcLoginCustomWithNoUniqueJwtDecoderFactory() {
451+
assertThatThrownBy(() -> loadConfig(OAuth2LoginConfig.class, NoUniqueJwtDecoderFactoryConfig.class))
452+
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class)
453+
.hasMessageContaining("No qualifying bean of type " +
454+
"'org.springframework.security.oauth2.jwt.JwtDecoderFactory<org.springframework.security.oauth2.client.registration.ClientRegistration>' " +
455+
"available: expected single matching bean but found 2: jwtDecoderFactory1,jwtDecoderFactory2");
456+
}
457+
450458
private void loadConfig(Class<?>... configs) {
451459
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
452460
applicationContext.register(configs);
@@ -455,25 +463,6 @@ private void loadConfig(Class<?>... configs) {
455463
this.context = applicationContext;
456464
}
457465

458-
private void registerJwtDecoder() {
459-
JwtDecoder decoder = token -> {
460-
Map<String, Object> claims = new HashMap<>();
461-
claims.put(IdTokenClaimNames.SUB, "sub123");
462-
claims.put(IdTokenClaimNames.ISS, "http://localhost/iss");
463-
claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId", "a", "u", "d"));
464-
claims.put(IdTokenClaimNames.AZP, "clientId");
465-
return new Jwt("token123", Instant.now(), Instant.now().plusSeconds(3600),
466-
Collections.singletonMap("header1", "value1"), claims);
467-
};
468-
this.springSecurityFilterChain.getFilters("/login/oauth2/code/google").stream()
469-
.filter(OAuth2LoginAuthenticationFilter.class::isInstance)
470-
.findFirst()
471-
.ifPresent(filter -> PropertyAccessorFactory.forDirectFieldAccess(filter)
472-
.setPropertyValue(
473-
"authenticationManager.providers[2].jwtDecoders['google']",
474-
decoder));
475-
}
476-
477466
private OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(String... scopes) {
478467
return this.createOAuth2AuthorizationRequest(GOOGLE_CLIENT_REGISTRATION, scopes);
479468
}
@@ -632,6 +621,42 @@ HttpSessionOAuth2AuthorizationRequestRepository oauth2AuthorizationRequestReposi
632621
}
633622
}
634623

624+
@Configuration
625+
static class JwtDecoderFactoryConfig {
626+
627+
@Bean
628+
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
629+
return clientRegistration -> getJwtDecoder();
630+
}
631+
632+
private static JwtDecoder getJwtDecoder() {
633+
return token -> {
634+
Map<String, Object> claims = new HashMap<>();
635+
claims.put(IdTokenClaimNames.SUB, "sub123");
636+
claims.put(IdTokenClaimNames.ISS, "http://localhost/iss");
637+
claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId", "a", "u", "d"));
638+
claims.put(IdTokenClaimNames.AZP, "clientId");
639+
return new Jwt("token123", Instant.now(), Instant.now().plusSeconds(3600),
640+
Collections.singletonMap("header1", "value1"), claims);
641+
};
642+
}
643+
}
644+
645+
@Configuration
646+
static class NoUniqueJwtDecoderFactoryConfig {
647+
648+
@Bean
649+
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory1() {
650+
return clientRegistration -> JwtDecoderFactoryConfig.getJwtDecoder();
651+
}
652+
653+
@Bean
654+
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory2() {
655+
return clientRegistration -> JwtDecoderFactoryConfig.getJwtDecoder();
656+
}
657+
658+
}
659+
635660
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createOauth2AccessTokenResponseClient() {
636661
return request -> {
637662
Map<String, Object> additionalParameters = new HashMap<>();

0 commit comments

Comments
 (0)