|
@@ -16,12 +16,6 @@
|
|
|
|
|
|
package org.springframework.security.config.web.server;
|
|
|
|
|
|
-import static org.assertj.core.api.Assertions.assertThat;
|
|
|
-import static org.mockito.ArgumentMatchers.any;
|
|
|
-import static org.mockito.Mockito.mock;
|
|
|
-import static org.mockito.Mockito.verify;
|
|
|
-import static org.mockito.Mockito.when;
|
|
|
-
|
|
|
import org.junit.Rule;
|
|
|
import org.junit.Test;
|
|
|
import org.openqa.selenium.WebDriver;
|
|
@@ -34,15 +28,29 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
|
|
|
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
|
|
import org.springframework.security.config.test.SpringTestRule;
|
|
|
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
|
|
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
|
|
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
|
|
+import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
|
|
+import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
|
|
+import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
|
|
|
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
|
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
|
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
|
|
+import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
|
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
|
|
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
|
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
|
|
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
|
|
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
|
|
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
|
|
+import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
|
|
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
|
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
|
|
+import org.springframework.security.oauth2.jwt.Jwt;
|
|
|
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
|
|
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
|
|
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
|
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
|
|
import org.springframework.security.web.server.WebFilterChainProxy;
|
|
@@ -51,9 +59,17 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
|
|
import org.springframework.web.server.ServerWebExchange;
|
|
|
import org.springframework.web.server.WebFilter;
|
|
|
import org.springframework.web.server.WebFilterChain;
|
|
|
-
|
|
|
import reactor.core.publisher.Mono;
|
|
|
|
|
|
+import java.time.Instant;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
/**
|
|
|
* @author Rob Winch
|
|
|
* @since 5.1
|
|
@@ -72,6 +88,12 @@ public class OAuth2LoginTests {
|
|
|
.clientSecret("secret")
|
|
|
.build();
|
|
|
|
|
|
+ private static ClientRegistration google = CommonOAuth2Provider.GOOGLE
|
|
|
+ .getBuilder("google")
|
|
|
+ .clientId("client")
|
|
|
+ .clientSecret("secret")
|
|
|
+ .build();
|
|
|
+
|
|
|
@Test
|
|
|
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
|
|
|
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
|
|
@@ -97,11 +119,6 @@ public class OAuth2LoginTests {
|
|
|
static class OAuth2LoginWithMulitpleClientRegistrations {
|
|
|
@Bean
|
|
|
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
|
|
- ClientRegistration google = CommonOAuth2Provider.GOOGLE
|
|
|
- .getBuilder("google")
|
|
|
- .clientId("client")
|
|
|
- .clientSecret("secret")
|
|
|
- .build();
|
|
|
return new InMemoryReactiveClientRegistrationRepository(github, google);
|
|
|
}
|
|
|
}
|
|
@@ -182,6 +199,107 @@ public class OAuth2LoginTests {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ public void oauth2LoginWhenCustomJwtDecoderFactoryThenUsed() {
|
|
|
+ this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class,
|
|
|
+ OAuth2LoginWithJwtDecoderFactoryBeanConfig.class).autowire();
|
|
|
+
|
|
|
+ WebTestClient webTestClient = WebTestClientBuilder
|
|
|
+ .bindToWebFilters(this.springSecurity)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ OAuth2LoginWithJwtDecoderFactoryBeanConfig config = this.spring.getContext()
|
|
|
+ .getBean(OAuth2LoginWithJwtDecoderFactoryBeanConfig.class);
|
|
|
+
|
|
|
+ OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
|
|
|
+ OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
|
|
|
+ OAuth2AuthorizationCodeAuthenticationToken token = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
|
|
+
|
|
|
+ ServerAuthenticationConverter converter = config.authenticationConverter;
|
|
|
+ when(converter.convert(any())).thenReturn(Mono.just(token));
|
|
|
+
|
|
|
+ Map<String, Object> additionalParameters = new HashMap<>();
|
|
|
+ additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
|
|
|
+ OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
|
|
|
+ .tokenType(accessToken.getTokenType())
|
|
|
+ .scopes(accessToken.getScopes())
|
|
|
+ .additionalParameters(additionalParameters)
|
|
|
+ .build();
|
|
|
+ ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
|
|
|
+ when(tokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
|
+
|
|
|
+ OidcUser user = TestOidcUsers.create();
|
|
|
+ ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = config.userService;
|
|
|
+ when(userService.loadUser(any())).thenReturn(Mono.just(user));
|
|
|
+
|
|
|
+ webTestClient.get()
|
|
|
+ .uri("/login/oauth2/code/google")
|
|
|
+ .exchange()
|
|
|
+ .expectStatus().is3xxRedirection();
|
|
|
+
|
|
|
+ verify(config.jwtDecoderFactory).createDecoder(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ static class OAuth2LoginWithJwtDecoderFactoryBeanConfig {
|
|
|
+
|
|
|
+ ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
|
|
|
+
|
|
|
+ ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient =
|
|
|
+ mock(ReactiveOAuth2AccessTokenResponseClient.class);
|
|
|
+
|
|
|
+ ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
|
|
|
+
|
|
|
+ ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = spy(new JwtDecoderFactory());
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
|
|
|
+ // @formatter:off
|
|
|
+ http
|
|
|
+ .authorizeExchange()
|
|
|
+ .anyExchange().authenticated()
|
|
|
+ .and()
|
|
|
+ .oauth2Login()
|
|
|
+ .authenticationConverter(authenticationConverter)
|
|
|
+ .authenticationManager(authenticationManager());
|
|
|
+ return http.build();
|
|
|
+ // @formatter:on
|
|
|
+ }
|
|
|
+
|
|
|
+ private ReactiveAuthenticationManager authenticationManager() {
|
|
|
+ OidcAuthorizationCodeReactiveAuthenticationManager oidc =
|
|
|
+ new OidcAuthorizationCodeReactiveAuthenticationManager(tokenResponseClient, userService);
|
|
|
+ oidc.setJwtDecoderFactory(jwtDecoderFactory());
|
|
|
+ return oidc;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ public ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
|
|
|
+ return jwtDecoderFactory;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class JwtDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
|
|
|
+ return getJwtDecoder();
|
|
|
+ }
|
|
|
+
|
|
|
+ private ReactiveJwtDecoder getJwtDecoder() {
|
|
|
+ return token -> {
|
|
|
+ Map<String, Object> claims = new HashMap<>();
|
|
|
+ claims.put(IdTokenClaimNames.SUB, "subject");
|
|
|
+ claims.put(IdTokenClaimNames.ISS, "http://localhost/issuer");
|
|
|
+ claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client"));
|
|
|
+ claims.put(IdTokenClaimNames.AZP, "client");
|
|
|
+ Jwt jwt = new Jwt("id-token", Instant.now(), Instant.now().plusSeconds(3600),
|
|
|
+ Collections.singletonMap("header1", "value1"), claims);
|
|
|
+ return Mono.just(jwt);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
static class GitHubWebFilter implements WebFilter {
|
|
|
|
|
|
@Override
|