Compare commits
39 Commits
5.6.3
...
5.2.2.RELEASE
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a2b71d931 | |||
| c4ccc96655 | |||
| 6c310213a8 | |||
| a5b6b9a398 | |||
| 9e6910273c | |||
| ea809b01a6 | |||
| 8054239a12 | |||
| 46486194c2 | |||
| 00b08bc725 | |||
| 6e0fbfcccd | |||
| 87ea083520 | |||
| 9db3f51f2a | |||
| 3cc4a945c6 | |||
| dbc43fb47d | |||
| ce6a0368bd | |||
| 9dd3dfe718 | |||
| edb6cd3729 | |||
| 2dbedf7af5 | |||
| 630eb10704 | |||
| f4d4c08329 | |||
| cc956a66df | |||
| 29182abb34 | |||
| b754a3d635 | |||
| 0d24e2b8cf | |||
| b00999deed | |||
| 59ca2ddf65 | |||
| 0782228914 | |||
| 29eb8b9177 | |||
| bd6ff1f319 | |||
| 6db7b457b7 | |||
| 840d3aa986 | |||
| 4c5c4f6cce | |||
| 148b570a98 | |||
| 752d5f29aa | |||
| e4aa3be4c5 | |||
| 0babe7d930 | |||
| b905cb8aaa | |||
| 19c2209a12 | |||
| 18f48e4a16 |
+1
@@ -162,6 +162,7 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
|
||||
matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
|
||||
matchers.add(notXRequestedWith);
|
||||
matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
|
||||
matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM));
|
||||
|
||||
return new AndRequestMatcher(matchers);
|
||||
}
|
||||
|
||||
+41
-10
@@ -76,9 +76,11 @@ import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserSer
|
||||
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter;
|
||||
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
@@ -984,9 +986,9 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerWebExchangeMatcher authenticationMatcher;
|
||||
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
/**
|
||||
* Configures the {@link ReactiveAuthenticationManager} to use. The default is
|
||||
@@ -1028,6 +1030,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationFailureHandler} used after authentication failure.
|
||||
* Defaults to {@link RedirectServerAuthenticationFailureHandler} redirecting to "/login?error".
|
||||
*
|
||||
* @since 5.2
|
||||
* @param authenticationFailureHandler the failure handler to use
|
||||
@@ -1084,7 +1087,9 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationConverter getAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||
if (this.authenticationConverter == null) {
|
||||
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
||||
ServerOAuth2AuthorizationCodeAuthenticationTokenConverter authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
||||
authenticationConverter.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
@@ -1172,24 +1177,44 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
|
||||
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
|
||||
|
||||
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
|
||||
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
|
||||
authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
|
||||
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||
MediaType.TEXT_HTML);
|
||||
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
Map<String, String> urlToText = http.oauth2Login.getLinks();
|
||||
String authenticationEntryPointRedirectPath;
|
||||
if (urlToText.size() == 1) {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
|
||||
authenticationEntryPointRedirectPath = urlToText.keySet().iterator().next();
|
||||
} else {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
|
||||
authenticationEntryPointRedirectPath = "/login";
|
||||
}
|
||||
RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(authenticationEntryPointRedirectPath);
|
||||
entryPoint.setRequestCache(http.requestCache.requestCache);
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, entryPoint));
|
||||
|
||||
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandler == null) {
|
||||
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
|
||||
handler.setRequestCache(http.requestCache.requestCache);
|
||||
this.authenticationSuccessHandler = handler;
|
||||
}
|
||||
return this.authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
|
||||
if (this.authenticationFailureHandler == null) {
|
||||
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
|
||||
}
|
||||
return this.authenticationFailureHandler;
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
|
||||
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
|
||||
}
|
||||
@@ -2765,7 +2790,9 @@ public class ServerHttpSecurity {
|
||||
protected void configure(ServerHttpSecurity http) {
|
||||
if (this.csrfTokenRepository != null) {
|
||||
this.filter.setCsrfTokenRepository(this.csrfTokenRepository);
|
||||
http.logout().addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
|
||||
if (ServerHttpSecurity.this.logout != null) {
|
||||
ServerHttpSecurity.this.logout.addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
|
||||
}
|
||||
}
|
||||
http.addFilterAt(this.filter, SecurityWebFiltersOrder.CSRF);
|
||||
}
|
||||
@@ -3022,8 +3049,12 @@ public class ServerHttpSecurity {
|
||||
public FormLoginSpec loginPage(String loginPage) {
|
||||
this.defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(loginPage);
|
||||
this.authenticationEntryPoint = this.defaultEntryPoint;
|
||||
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
|
||||
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
|
||||
if (this.requiresAuthenticationMatcher == null) {
|
||||
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
|
||||
}
|
||||
if (this.authenticationFailureHandler == null) {
|
||||
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
+21
@@ -458,4 +458,25 @@ public class LogoutConfigurerTests {
|
||||
@EnableWebSecurity
|
||||
static class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
|
||||
this.spring.register(LogoutDisabledConfig.class).autowire();
|
||||
|
||||
this.mvc.perform(post("/logout")
|
||||
.with(csrf()))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class LogoutDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout()
|
||||
.disable();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -183,6 +183,21 @@ public class RequestCacheConfigurerTests {
|
||||
|
||||
// This is desirable since XHR requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsTextEventStreamThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores text/event-stream
|
||||
|
||||
// This is desirable since event-stream requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
|
||||
|
||||
+1
-1
@@ -1127,7 +1127,7 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.with(bearerToken("token")))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
|
||||
containsString("Provided token [token] isn't active")));
|
||||
containsString("Provided token isn't active")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+58
@@ -33,9 +33,11 @@ import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverB
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.csrf.CsrfToken;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -213,6 +215,62 @@ public class FormLoginTests {
|
||||
homePage.assertAt();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formLoginWhenCustomAuthenticationFailureHandlerThenUsed() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
.pathMatchers("/login", "/failure").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/failure"))
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class)
|
||||
.assertAt();
|
||||
|
||||
loginPage.loginForm()
|
||||
.username("invalid")
|
||||
.password("invalid")
|
||||
.submit(HomePage.class);
|
||||
|
||||
assertThat(driver.getCurrentUrl()).endsWith("/failure");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formLoginWhenCustomRequiresAuthenticationMatcherThenUsed() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
.pathMatchers("/login", "/sign-in").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.requiresAuthenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/sign-in"))
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
driver.get("http://localhost/sign-in");
|
||||
|
||||
assertThat(driver.getCurrentUrl()).endsWith("/login?error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticationSuccess() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
|
||||
+36
@@ -164,4 +164,40 @@ public class LogoutSpecTests {
|
||||
.assertAt()
|
||||
.assertLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenDisabledThenPostToLogoutDoesNothing() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin().and()
|
||||
.logout().disable()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
|
||||
.assertAt();
|
||||
|
||||
FormLoginTests.HomePage homePage = loginPage.loginForm()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.submit(FormLoginTests.HomePage.class);
|
||||
|
||||
homePage.assertAt();
|
||||
|
||||
FormLoginTests.DefaultLogoutPage.to(driver)
|
||||
.assertAt()
|
||||
.logout();
|
||||
|
||||
homePage
|
||||
.assertAt();
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -70,6 +70,7 @@ 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.JwtValidationException;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
@@ -518,6 +519,85 @@ public class OAuth2LoginTests {
|
||||
verify(securityContextRepository).save(any(), any());
|
||||
}
|
||||
|
||||
// gh-5562
|
||||
@Test
|
||||
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
|
||||
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
|
||||
OAuth2LoginWithCustomBeansConfig.class).autowire();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(this.springSecurity)
|
||||
.build();
|
||||
|
||||
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
|
||||
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
|
||||
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
||||
|
||||
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
|
||||
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
|
||||
OAuth2Error oauth2Error = new OAuth2Error("invalid_request", "Invalid request", null);
|
||||
when(tokenResponseClient.getTokenResponse(any())).thenThrow(new OAuth2AuthenticationException(oauth2Error));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/login/oauth2/code/google")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.valueEquals("Location", "/login?error");
|
||||
}
|
||||
|
||||
// gh-6484
|
||||
@Test
|
||||
public void oauth2LoginWhenIdTokenValidationFailsThenDefaultRedirectToLogin() {
|
||||
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
|
||||
OAuth2LoginWithCustomBeansConfig.class).autowire();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(this.springSecurity)
|
||||
.build();
|
||||
|
||||
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
|
||||
|
||||
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
|
||||
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
|
||||
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
||||
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
|
||||
|
||||
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));
|
||||
|
||||
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = config.jwtDecoderFactory;
|
||||
OAuth2Error oauth2Error = new OAuth2Error("invalid_id_token", "Invalid ID Token", null);
|
||||
when(jwtDecoderFactory.createDecoder(any())).thenReturn(token ->
|
||||
Mono.error(new JwtValidationException("ID Token validation failed", Collections.singleton(oauth2Error))));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/login/oauth2/code/google")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.valueEquals("Location", "/login?error");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class OAuth2LoginWithCustomBeansConfig {
|
||||
|
||||
|
||||
+64
-6
@@ -20,10 +20,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.test.util.ReflectionTestUtils.getField;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -35,12 +37,20 @@ import org.apache.http.HttpHeaders;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.publisher.TestPublisher;
|
||||
|
||||
@@ -60,7 +70,6 @@ import org.springframework.security.web.server.context.WebSessionServerSecurityC
|
||||
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
|
||||
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
||||
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.reactive.server.EntityExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.FluxExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
@@ -196,7 +205,7 @@ public class ServerHttpSecurityTests {
|
||||
.isNotPresent();
|
||||
|
||||
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
|
||||
assertThat(logoutHandler)
|
||||
.get()
|
||||
@@ -209,17 +218,17 @@ public class ServerHttpSecurityTests {
|
||||
|
||||
assertThat(getWebFilter(securityWebFilterChain, CsrfWebFilter.class))
|
||||
.get()
|
||||
.extracting(csrfWebFilter -> ReflectionTestUtils.getField(csrfWebFilter, "csrfTokenRepository"))
|
||||
.extracting(csrfWebFilter -> getField(csrfWebFilter, "csrfTokenRepository"))
|
||||
.isEqualTo(this.csrfTokenRepository);
|
||||
|
||||
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
|
||||
assertThat(logoutHandler)
|
||||
.get()
|
||||
.isExactlyInstanceOf(DelegatingServerLogoutHandler.class)
|
||||
.extracting(delegatingLogoutHandler ->
|
||||
((List<ServerLogoutHandler>) ReflectionTestUtils.getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
|
||||
((List<ServerLogoutHandler>) getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
|
||||
.map(ServerLogoutHandler::getClass)
|
||||
.collect(Collectors.toList()))
|
||||
.isEqualTo(Arrays.asList(SecurityContextServerLogoutHandler.class, CsrfServerLogoutHandler.class));
|
||||
@@ -475,9 +484,58 @@ public class ServerHttpSecurityTests {
|
||||
verify(customServerCsrfTokenRepository).loadToken(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
|
||||
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
|
||||
|
||||
SecurityWebFilterChain securityFilterChain = this.http
|
||||
.oauth2Login()
|
||||
.clientRegistrationRepository(clientRegistrationRepository)
|
||||
.and()
|
||||
.authorizeExchange().anyExchange().authenticated()
|
||||
.and()
|
||||
.requestCache(c -> c.requestCache(requestCache))
|
||||
.build();
|
||||
|
||||
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
|
||||
client.get().uri("/test").exchange();
|
||||
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
|
||||
verify(requestCache).saveRequest(captor.capture());
|
||||
assertThat(captor.getValue().getRequest().getURI().toString()).isEqualTo("/test");
|
||||
|
||||
|
||||
OAuth2LoginAuthenticationWebFilter authenticationWebFilter =
|
||||
getWebFilter(securityFilterChain, OAuth2LoginAuthenticationWebFilter.class).get();
|
||||
Object handler = getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
assertThat(getField(handler, "requestCache")).isSameAs(requestCache);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConfigureAuthorizationRequestRepositoryForOAuth2Login() {
|
||||
ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = mock(ServerAuthorizationRequestRepository.class);
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request().build();
|
||||
|
||||
when(authorizationRequestRepository.removeAuthorizationRequest(any())).thenReturn(Mono.just(authorizationRequest));
|
||||
|
||||
SecurityWebFilterChain securityFilterChain = this.http
|
||||
.oauth2Login()
|
||||
.clientRegistrationRepository(clientRegistrationRepository)
|
||||
.authorizationRequestRepository(authorizationRequestRepository)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
|
||||
client.get().uri("/login/oauth2/code/registration-id").exchange();
|
||||
|
||||
verify(authorizationRequestRepository).removeAuthorizationRequest(any());
|
||||
}
|
||||
|
||||
private boolean isX509Filter(WebFilter filter) {
|
||||
try {
|
||||
Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");
|
||||
Object converter = getField(filter, "authenticationConverter");
|
||||
return converter.getClass().isAssignableFrom(ServerX509AuthenticationConverter.class);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// field doesn't exist
|
||||
|
||||
@@ -38,12 +38,12 @@ This configuration enables <<rsocket-authentication-basic,basic authentication>>
|
||||
|
||||
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
|
||||
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
|
||||
In a Spring Boot application this can be done using the following code.
|
||||
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ServerRSocketFactoryCustomizer springSecurityRSocketSecurity(
|
||||
ServerRSocketFactoryProcessor springSecurityRSocketSecurity(
|
||||
SecuritySocketAcceptorInterceptor interceptor) {
|
||||
return builder -> builder.addSocketAcceptorPlugin(interceptor);
|
||||
}
|
||||
|
||||
@@ -2372,8 +2372,8 @@ A bean identifier, used for referring to the bean elsewhere in the context.
|
||||
[[nsa-ldap-server-ldif]]
|
||||
* **ldif**
|
||||
Explicitly specifies an ldif file resource to load into an embedded LDAP server.
|
||||
The ldiff is should be a Spring resource pattern (i.e. classpath:init.ldiff).
|
||||
The default is classpath*:*.ldiff
|
||||
The ldif should be a Spring resource pattern (i.e. classpath:init.ldif).
|
||||
The default is classpath*:*.ldif
|
||||
|
||||
|
||||
[[nsa-ldap-server-manager-dn]]
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
aspectjVersion=1.9.3
|
||||
aspectjVersion=1.9.5
|
||||
gaeVersion=1.9.76
|
||||
springBootVersion=2.2.0.RELEASE
|
||||
version=5.2.2.BUILD-SNAPSHOT
|
||||
springBootVersion=2.2.4.RELEASE
|
||||
version=5.2.2.RELEASE
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Dysprosium-SR1'
|
||||
ext.reactorVersion = 'Dysprosium-SR4'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.2.1.RELEASE'
|
||||
ext.springVersion = '5.2.3.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Moore-SR1'
|
||||
ext.springDataVersion = 'Moore-SR3'
|
||||
}
|
||||
|
||||
ext.rsocketVersion = '1.0.0-RC5'
|
||||
@@ -20,16 +20,16 @@ dependencyManagement {
|
||||
}
|
||||
dependencies {
|
||||
dependency 'cglib:cglib-nodep:3.3.0'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.14.2'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.14.6'
|
||||
dependency 'opensymphony:sitemesh:2.4.2'
|
||||
dependency 'org.gebish:geb-spock:0.10.0'
|
||||
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.4'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.4'
|
||||
dependency 'org.powermock:powermock-core:2.0.4'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.4'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.4'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.4'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.5'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.5'
|
||||
dependency 'org.powermock:powermock-core:2.0.5'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.5'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.5'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.5'
|
||||
dependency 'org.python:jython:2.5.0'
|
||||
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
|
||||
@@ -42,9 +42,9 @@ dependencyManagement {
|
||||
dependency 'asm:asm:3.1'
|
||||
dependency 'ch.qos.logback:logback-classic:1.2.3'
|
||||
dependency 'ch.qos.logback:logback-core:1.2.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.0'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.0'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.2'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.2'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.2'
|
||||
dependency 'com.fasterxml:classmate:1.3.4'
|
||||
dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1'
|
||||
dependency 'com.google.appengine:appengine-api-1.0-sdk:$gaeVersion'
|
||||
@@ -58,14 +58,14 @@ dependencyManagement {
|
||||
dependency 'com.nimbusds:lang-tag:1.4.3'
|
||||
dependency 'com.nimbusds:nimbus-jose-jwt:7.8.1'
|
||||
dependency 'com.nimbusds:oauth2-oidc-sdk:6.14'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.14.1'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.14.6'
|
||||
dependency 'com.squareup.okio:okio:1.13.0'
|
||||
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
|
||||
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.12'
|
||||
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
|
||||
dependency 'commons-cli:commons-cli:1.4'
|
||||
dependency 'commons-codec:commons-codec:1.13'
|
||||
dependency 'commons-codec:commons-codec:1.14'
|
||||
dependency 'commons-collections:commons-collections:3.2.2'
|
||||
dependency 'commons-httpclient:commons-httpclient:3.1'
|
||||
dependency 'commons-io:commons-io:2.6'
|
||||
@@ -138,7 +138,7 @@ dependencyManagement {
|
||||
dependency 'org.apache.directory.shared:shared-cursor:0.9.15'
|
||||
dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15'
|
||||
dependency 'org.apache.directory.shared:shared-ldap:0.9.15'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.10'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.11'
|
||||
dependency 'org.apache.httpcomponents:httpcore:4.4.8'
|
||||
dependency 'org.apache.httpcomponents:httpmime:4.5.3'
|
||||
dependency 'org.apache.mina:mina-core:2.0.0-M6'
|
||||
@@ -182,8 +182,8 @@ dependencyManagement {
|
||||
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
|
||||
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.17.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.8.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.0.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.10.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.2.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.5.0'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
|
||||
dependency 'org.javassist:javassist:3.22.0-CR2'
|
||||
@@ -203,11 +203,11 @@ dependencyManagement {
|
||||
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
|
||||
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
|
||||
dependency 'org.skyscreamer:jsonassert:1.5.0'
|
||||
dependency 'org.slf4j:jcl-over-slf4j:1.7.28'
|
||||
dependency 'org.slf4j:jul-to-slf4j:1.7.28'
|
||||
dependency 'org.slf4j:log4j-over-slf4j:1.7.28'
|
||||
dependency 'org.slf4j:slf4j-api:1.7.28'
|
||||
dependency 'org.slf4j:slf4j-nop:1.7.28'
|
||||
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
|
||||
dependency 'org.slf4j:jul-to-slf4j:1.7.30'
|
||||
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
|
||||
dependency 'org.slf4j:slf4j-api:1.7.30'
|
||||
dependency 'org.slf4j:slf4j-nop:1.7.30'
|
||||
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
|
||||
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
|
||||
dependency 'org.synchronoss.cloud:nio-multipart-parser:1.1.0'
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.ldap.server;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.ldap.core.ContextSource;
|
||||
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
|
||||
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
|
||||
|
||||
/**
|
||||
* Tests for {@link UnboundIdContainer}, specifically relating to LDIF file detection.
|
||||
*
|
||||
* @author Eleftheria Stein
|
||||
*/
|
||||
public class UnboundIdContainerLdifTests {
|
||||
|
||||
AnnotationConfigApplicationContext appCtx;
|
||||
|
||||
@After
|
||||
public void closeAppContext() {
|
||||
if (appCtx != null) {
|
||||
appCtx.close();
|
||||
appCtx = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unboundIdContainerWhenCustomLdifNameThenLdifLoaded() {
|
||||
appCtx = new AnnotationConfigApplicationContext(CustomLdifConfig.class);
|
||||
|
||||
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
|
||||
.getBean(ContextSource.class);
|
||||
|
||||
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
|
||||
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomLdifConfig {
|
||||
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||
"classpath:test-server.ldif");
|
||||
|
||||
@Bean
|
||||
UnboundIdContainer ldapContainer() {
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ContextSource contextSource(UnboundIdContainer container) {
|
||||
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
|
||||
+ container.getPort() + "/dc=springframework,dc=org");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unboundIdContainerWhenWildcardLdifNameThenLdifLoaded() {
|
||||
appCtx = new AnnotationConfigApplicationContext(WildcardLdifConfig.class);
|
||||
|
||||
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
|
||||
.getBean(ContextSource.class);
|
||||
|
||||
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
|
||||
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class WildcardLdifConfig {
|
||||
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||
"classpath*:test-server.ldif");
|
||||
|
||||
@Bean
|
||||
UnboundIdContainer ldapContainer() {
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ContextSource contextSource(UnboundIdContainer container) {
|
||||
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
|
||||
+ container.getPort() + "/dc=springframework,dc=org");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unboundIdContainerWhenMalformedLdifThenException() {
|
||||
try {
|
||||
appCtx = new AnnotationConfigApplicationContext(MalformedLdifConfig.class);
|
||||
failBecauseExceptionWasNotThrown(IllegalStateException.class);
|
||||
} catch (Exception e) {
|
||||
assertThat(e.getCause()).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(e.getMessage()).contains("Unable to load LDIF classpath:test-server-malformed.txt");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class MalformedLdifConfig {
|
||||
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||
"classpath:test-server-malformed.txt");
|
||||
|
||||
@Bean
|
||||
UnboundIdContainer ldapContainer() {
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
dn: ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn ou=subgroups,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: subgroups
|
||||
@@ -114,10 +114,10 @@ public class UnboundIdContainer implements InitializingBean, DisposableBean, Lif
|
||||
|
||||
private void importLdif(InMemoryDirectoryServer directoryServer) {
|
||||
if (StringUtils.hasText(this.ldif)) {
|
||||
Resource resource = this.context.getResource(this.ldif);
|
||||
try {
|
||||
if (resource.exists()) {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
Resource[] resources = this.context.getResources(this.ldif);
|
||||
if (resources.length > 0 && resources[0].exists()) {
|
||||
try (InputStream inputStream = resources[0].getInputStream()) {
|
||||
directoryServer.importFromLDIF(false, new LDIFReader(inputStream));
|
||||
}
|
||||
}
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.client;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
|
||||
* that is capable of operating outside of a {@code ServerHttpRequest} context,
|
||||
* e.g. in a scheduled/background thread and/or in the service-tier.
|
||||
*
|
||||
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
|
||||
*
|
||||
* @author Ankur Pathak
|
||||
* @author Phil Clay
|
||||
* @see ReactiveOAuth2AuthorizedClientManager
|
||||
* @see ReactiveOAuth2AuthorizedClientProvider
|
||||
* @see ReactiveOAuth2AuthorizedClientService
|
||||
* @since 5.2.2
|
||||
*/
|
||||
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
|
||||
implements ReactiveOAuth2AuthorizedClientManager {
|
||||
|
||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
|
||||
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
|
||||
|
||||
/**
|
||||
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
|
||||
*
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
* @param authorizedClientService the authorized client service
|
||||
*/
|
||||
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
this.authorizedClientService = authorizedClientService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
|
||||
|
||||
return createAuthorizationContext(authorizeRequest)
|
||||
.flatMap(this::authorizeAndSave);
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
|
||||
Authentication principal = authorizeRequest.getPrincipal();
|
||||
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
|
||||
.map(OAuth2AuthorizationContext::withAuthorizedClient)
|
||||
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
|
||||
.map(OAuth2AuthorizationContext::withAuthorizedClient)
|
||||
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
|
||||
.flatMap(contextBuilder -> this.contextAttributesMapper.apply(authorizeRequest)
|
||||
.defaultIfEmpty(Collections.emptyMap())
|
||||
.map(contextAttributes -> {
|
||||
OAuth2AuthorizationContext.Builder builder = contextBuilder.principal(principal);
|
||||
if (!contextAttributes.isEmpty()) {
|
||||
builder = builder.attributes(attributes -> attributes.putAll(contextAttributes));
|
||||
}
|
||||
return builder.build();
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
|
||||
return this.authorizedClientProvider.authorize(authorizationContext)
|
||||
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
|
||||
authorizedClient,
|
||||
authorizationContext.getPrincipal())
|
||||
.thenReturn(authorizedClient))
|
||||
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
|
||||
*
|
||||
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client
|
||||
*/
|
||||
public void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
|
||||
this.authorizedClientProvider = authorizedClientProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Function} used for mapping attribute(s) from the {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes
|
||||
* to be associated to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
|
||||
*
|
||||
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
|
||||
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
|
||||
*/
|
||||
public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
|
||||
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
|
||||
this.contextAttributesMapper = contextAttributesMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
|
||||
*/
|
||||
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
|
||||
|
||||
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper =
|
||||
new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
|
||||
|
||||
@Override
|
||||
public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
|
||||
*/
|
||||
final class OAuth2AuthorizationExchangeValidator {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||
|
||||
static void validate(OAuth2AuthorizationExchange authorizationExchange) {
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
|
||||
@@ -44,10 +43,5 @@ final class OAuth2AuthorizationExchangeValidator {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthorizationException(oauth2Error);
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthorizationException(oauth2Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -78,7 +78,6 @@ import java.util.Map;
|
||||
*/
|
||||
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
||||
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
@@ -132,11 +131,6 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse;
|
||||
try {
|
||||
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
||||
|
||||
-6
@@ -80,7 +80,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
||||
ReactiveAuthenticationManager {
|
||||
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
||||
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
|
||||
|
||||
@@ -131,11 +130,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange());
|
||||
|
||||
+3
-2
@@ -99,15 +99,16 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React
|
||||
private Mono<OAuth2AuthorizedClient> loadAuthorizedClient(String clientRegistrationId, Authentication principal, ServerWebExchange serverWebExchange) {
|
||||
return Mono.justOrEmpty(serverWebExchange)
|
||||
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
|
||||
.flatMap(exchange -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, exchange));
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizedClient> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, ServerWebExchange serverWebExchange) {
|
||||
return Mono.justOrEmpty(serverWebExchange)
|
||||
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
|
||||
.flatMap(exchange -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, exchange)
|
||||
.thenReturn(authorizedClient))
|
||||
.defaultIfEmpty(authorizedClient);
|
||||
.thenReturn(authorizedClient));
|
||||
}
|
||||
|
||||
private static Mono<ServerWebExchange> currentServerWebExchange() {
|
||||
|
||||
+66
-1
@@ -22,6 +22,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
@@ -35,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
@@ -124,6 +126,17 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.clientCredentials()
|
||||
.password()
|
||||
.build();
|
||||
|
||||
// gh-7544
|
||||
if (authorizedClientRepository instanceof UnAuthenticatedServerOAuth2AuthorizedClientRepository) {
|
||||
UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager =
|
||||
new UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository,
|
||||
(UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository);
|
||||
unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
return unauthenticatedAuthorizedClientManager;
|
||||
}
|
||||
|
||||
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
@@ -266,7 +279,11 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.clientCredentials(this::updateClientCredentialsProvider)
|
||||
.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
||||
.build();
|
||||
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||
if (this.authorizedClientManager instanceof UnAuthenticatedReactiveOAuth2AuthorizedClientManager) {
|
||||
((UnAuthenticatedReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||
} else {
|
||||
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateClientCredentialsProvider(ReactiveOAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder builder) {
|
||||
@@ -376,4 +393,52 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class UnAuthenticatedReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
|
||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
private final UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
|
||||
private UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
||||
UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
|
||||
|
||||
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
|
||||
Authentication principal = authorizeRequest.getPrincipal();
|
||||
|
||||
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
|
||||
.switchIfEmpty(Mono.defer(() -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, null)))
|
||||
.flatMap(authorizedClient -> {
|
||||
// Re-authorize
|
||||
return Mono.just(OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient).principal(principal).build())
|
||||
.flatMap(this.authorizedClientProvider::authorize)
|
||||
.flatMap(reauthorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(reauthorizedClient, principal, null).thenReturn(reauthorizedClient))
|
||||
// Default to the existing authorizedClient if the client was not re-authorized
|
||||
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
|
||||
authorizeRequest.getAuthorizedClient() : authorizedClient);
|
||||
})
|
||||
.switchIfEmpty(Mono.deferWithContext(context ->
|
||||
// Authorize
|
||||
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
||||
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
|
||||
.flatMap(clientRegistration -> Mono.just(OAuth2AuthorizationContext.withClientRegistration(clientRegistration).principal(principal).build()))
|
||||
.flatMap(this.authorizedClientProvider::authorize)
|
||||
.flatMap(authorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, null).thenReturn(authorizedClient))
|
||||
.subscriberContext(context)
|
||||
));
|
||||
}
|
||||
|
||||
private void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
|
||||
this.authorizedClientProvider = authorizedClientProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -137,13 +137,11 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
|
||||
.switchIfEmpty(currentServerWebExchange());
|
||||
|
||||
return Mono.zip(defaultedRegistrationId, defaultedAuthentication, defaultedExchange)
|
||||
.map(t3 -> {
|
||||
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1()).principal(t3.getT2());
|
||||
if (t3.getT3() != null) {
|
||||
builder.attribute(ServerWebExchange.class.getName(), t3.getT3());
|
||||
}
|
||||
return builder.build();
|
||||
});
|
||||
.map(t3 -> OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1())
|
||||
.principal(t3.getT2())
|
||||
.attribute(ServerWebExchange.class.getName(), t3.getT3())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<Authentication> currentAuthentication() {
|
||||
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.client;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.test.publisher.PublisherProbe;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}.
|
||||
*
|
||||
* @author Ankur Pathak
|
||||
* @author Phil Clay
|
||||
*/
|
||||
public class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManagerTests {
|
||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper;
|
||||
private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
||||
private ClientRegistration clientRegistration;
|
||||
private Authentication principal;
|
||||
private OAuth2AuthorizedClient authorizedClient;
|
||||
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
|
||||
private PublisherProbe<Void> saveAuthorizedClientProbe;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Before
|
||||
public void setup() {
|
||||
this.clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
|
||||
this.authorizedClientService = mock(ReactiveOAuth2AuthorizedClientService.class);
|
||||
this.saveAuthorizedClientProbe = PublisherProbe.empty();
|
||||
when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(this.saveAuthorizedClientProbe.mono());
|
||||
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
|
||||
this.contextAttributesMapper = mock(Function.class);
|
||||
when(this.contextAttributesMapper.apply(any())).thenReturn(Mono.empty());
|
||||
this.authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, this.authorizedClientService);
|
||||
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
|
||||
this.authorizedClientManager.setContextAttributesMapper(this.contextAttributesMapper);
|
||||
this.clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
this.principal = new TestingAuthenticationToken("principal", "password");
|
||||
this.authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.scopes("read", "write"), TestOAuth2RefreshTokens.refreshToken());
|
||||
this.authorizationContextCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationContext.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(null, this.authorizedClientService))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("clientRegistrationRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(this.clientRegistrationRepository, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizedClientService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthorizedClientProviderWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.setAuthorizedClientProvider(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizedClientProvider cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setContextAttributesMapperWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.setContextAttributesMapper(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("contextAttributesMapper cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenRequestIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.authorize(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizeRequest cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
|
||||
String clientRegistrationId = "invalid-registration-id";
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)).thenReturn(Mono.empty());
|
||||
StepVerifier.create(this.authorizedClientManager.authorize(authorizeRequest))
|
||||
.verifyError(IllegalArgumentException.class);
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenNotAuthorizedAndUnsupportedProviderThenNotAuthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
when(this.authorizedClientService.loadAuthorizedClient(
|
||||
any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
when(authorizedClientProvider.authorize(any())).thenReturn(Mono.empty());
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient).verifyComplete();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isNull();
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService, never()).saveAuthorizedClient(
|
||||
any(OAuth2AuthorizedClient.class), eq(this.principal));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenNotAuthorizedAndSupportedProviderThenAuthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
|
||||
when(this.authorizedClientService.loadAuthorizedClient(
|
||||
any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(this.authorizedClient)
|
||||
.verifyComplete();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isNull();
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(this.authorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
when(this.authorizedClientService.loadAuthorizedClient(
|
||||
eq(this.clientRegistration.getRegistrationId()), eq(this.principal.getName()))).thenReturn(Mono.just(this.authorizedClient));
|
||||
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(reauthorizedClient)
|
||||
.verifyComplete();
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(reauthorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
|
||||
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(this.authorizedClient)
|
||||
.verifyComplete();
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService, never()).saveAuthorizedClient(
|
||||
any(OAuth2AuthorizedClient.class), eq(this.principal));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void reauthorizeWhenSupportedProviderThenReauthorized() {
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(reauthorizedClient)
|
||||
.verifyComplete();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(reauthorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void reauthorizeWhenRequestAttributeScopeThenMappedToContext() {
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
|
||||
.principal(this.principal)
|
||||
.attribute(OAuth2ParameterNames.SCOPE, "read write")
|
||||
.build();
|
||||
|
||||
this.authorizedClientManager.setContextAttributesMapper(new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(reauthorizedClient)
|
||||
.verifyComplete();
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(reauthorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
assertThat(authorizationContext.getAttributes()).containsKey(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
|
||||
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
|
||||
assertThat(requestScopeAttribute).contains("read", "write");
|
||||
|
||||
}
|
||||
}
|
||||
+1
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -98,19 +98,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthorizationException() {
|
||||
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
|
||||
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(
|
||||
this.authorizationRequest, authorizationResponse);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
this.authenticationProvider.authenticate(
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
this.clientRegistration, authorizationExchange));
|
||||
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().refreshToken("refresh").build();
|
||||
|
||||
+1
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -81,13 +81,6 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests {
|
||||
.isInstanceOf(OAuth2AuthorizationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenRedirectUriNotEqualThenOAuth2AuthorizationException() {
|
||||
this.authorizationRequest.redirectUri("https://example.org/notequal");
|
||||
assertThatCode(() -> authenticate())
|
||||
.isInstanceOf(OAuth2AuthorizationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidThenSuccess() {
|
||||
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build()));
|
||||
|
||||
+1
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -151,20 +151,6 @@ public class OAuth2LoginAuthenticationProviderTests {
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||
this.exception.expect(OAuth2AuthenticationException.class);
|
||||
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
|
||||
|
||||
OAuth2AuthorizationResponse authorizationResponse =
|
||||
success().redirectUri("https://example2.com").build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
|
||||
|
||||
this.authenticationProvider.authenticate(
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
|
||||
|
||||
-13
@@ -186,19 +186,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||
this.exception.expect(OAuth2AuthenticationException.class);
|
||||
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
|
||||
|
||||
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
|
||||
|
||||
this.authenticationProvider.authenticate(
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
|
||||
this.exception.expect(OAuth2AuthenticationException.class);
|
||||
|
||||
+18
-29
@@ -65,6 +65,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
private MockServerWebExchange serverWebExchange;
|
||||
private Context context;
|
||||
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
|
||||
private PublisherProbe<OAuth2AuthorizedClient> loadAuthorizedClientProbe;
|
||||
private PublisherProbe<Void> saveAuthorizedClientProbe;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -74,8 +75,9 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
anyString())).thenReturn(Mono.empty());
|
||||
this.authorizedClientRepository = mock(ServerOAuth2AuthorizedClientRepository.class);
|
||||
this.loadAuthorizedClientProbe = PublisherProbe.empty();
|
||||
when(this.authorizedClientRepository.loadAuthorizedClient(
|
||||
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.loadAuthorizedClientProbe.mono());
|
||||
this.saveAuthorizedClientProbe = PublisherProbe.empty();
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(
|
||||
any(OAuth2AuthorizedClient.class), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.saveAuthorizedClientProbe.mono());
|
||||
@@ -131,6 +133,16 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
.hasMessage("authorizeRequest cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenExchangeIsNullThenThrowIllegalArgumentException() {
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("serverWebExchange cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("invalid-registration-id")
|
||||
@@ -162,7 +174,8 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
assertThat(authorizedClient).isNull();
|
||||
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
|
||||
this.loadAuthorizedClientProbe.assertWasSubscribed();
|
||||
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -193,38 +206,14 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenNotAuthorizedAndSupportedProviderAndExchangeUnavailableThenAuthorizedButNotSaved() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
|
||||
when(this.authorizedClientProvider.authorize(
|
||||
any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isNull();
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
assertThat(authorizedClient).isSameAs(this.authorizedClient);
|
||||
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
this.loadAuthorizedClientProbe = PublisherProbe.of(Mono.just(this.authorizedClient));
|
||||
when(this.authorizedClientRepository.loadAuthorizedClient(
|
||||
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(Mono.just(this.authorizedClient));
|
||||
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(this.loadAuthorizedClientProbe.mono());
|
||||
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
@@ -313,7 +302,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
assertThat(authorizedClient).isSameAs(this.authorizedClient);
|
||||
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
|
||||
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
+38
@@ -57,6 +57,7 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
@@ -587,6 +588,43 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
verify(this.authorizedClientRepository).loadAuthorizedClient(eq(this.registration.getRegistrationId()), any(), eq(this.serverWebExchange));
|
||||
}
|
||||
|
||||
// gh-7544
|
||||
@Test
|
||||
public void filterWhenClientCredentialsClientNotAuthorizedAndOutsideRequestContextThenGetNewToken() {
|
||||
// Use UnAuthenticatedServerOAuth2AuthorizedClientRepository when operating outside of a request context
|
||||
ServerOAuth2AuthorizedClientRepository unauthenticatedAuthorizedClientRepository = spy(new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
|
||||
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
|
||||
this.clientRegistrationRepository, unauthenticatedAuthorizedClientRepository);
|
||||
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
|
||||
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||
.expiresIn(360)
|
||||
.build();
|
||||
when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(Mono.just(registration));
|
||||
|
||||
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||
.attributes(clientRegistrationId(registration.getRegistrationId()))
|
||||
.build();
|
||||
|
||||
this.function.filter(request, this.exchange).block();
|
||||
|
||||
verify(unauthenticatedAuthorizedClientRepository).loadAuthorizedClient(any(), any(), any());
|
||||
verify(this.clientCredentialsTokenResponseClient).getTokenResponse(any());
|
||||
verify(unauthenticatedAuthorizedClientRepository).saveAuthorizedClient(any(), any(), any());
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(1);
|
||||
ClientRequest request1 = requests.get(0);
|
||||
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
|
||||
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(getBody(request1)).isEmpty();
|
||||
}
|
||||
|
||||
private Context serverWebExchange() {
|
||||
return Context.of(ServerWebExchange.class, this.serverWebExchange);
|
||||
}
|
||||
|
||||
+2
-2
@@ -133,7 +133,7 @@ public class NimbusOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
public OAuth2AuthenticatedPrincipal introspect(String token) {
|
||||
RequestEntity<?> requestEntity = this.requestEntityConverter.convert(token);
|
||||
if (requestEntity == null) {
|
||||
throw new OAuth2IntrospectionException("Provided token [" + token + "] isn't active");
|
||||
throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity");
|
||||
}
|
||||
|
||||
ResponseEntity<String> responseEntity = makeRequest(requestEntity);
|
||||
@@ -143,7 +143,7 @@ public class NimbusOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
|
||||
// relying solely on the authorization server to validate this token (not checking 'exp', for example)
|
||||
if (!introspectionSuccessResponse.isActive()) {
|
||||
throw new OAuth2IntrospectionException("Provided token [" + token + "] isn't active");
|
||||
throw new OAuth2IntrospectionException("Provided token isn't active");
|
||||
}
|
||||
|
||||
return convertClaimsSet(introspectionSuccessResponse);
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@ public class NimbusReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
private void validate(String token, TokenIntrospectionSuccessResponse response) {
|
||||
// relying solely on the authorization server to validate this token (not checking 'exp', for example)
|
||||
if (!response.isActive()) {
|
||||
throw new OAuth2IntrospectionException("Provided token [" + token + "] isn't active");
|
||||
throw new OAuth2IntrospectionException("Provided token isn't active");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ public class NimbusOpaqueTokenIntrospectorTests {
|
||||
assertThatCode(() -> introspectionClient.introspect("token"))
|
||||
.isInstanceOf(OAuth2IntrospectionException.class)
|
||||
.extracting("message")
|
||||
.containsExactly("Provided token [token] isn't active");
|
||||
.containsExactly("Provided token isn't active");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ public class NimbusReactiveOpaqueTokenIntrospectorTests {
|
||||
assertThatCode(() -> introspectionClient.introspect("token").block())
|
||||
.isInstanceOf(OAuth2IntrospectionException.class)
|
||||
.extracting("message")
|
||||
.containsExactly("Provided token [token] isn't active");
|
||||
.containsExactly("Provided token isn't active");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -178,7 +178,7 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse);
|
||||
String username = getUsername(token, assertion);
|
||||
return new Saml2Authentication(
|
||||
() -> username, token.getSaml2Response(),
|
||||
new SimpleSaml2AuthenticatedPrincipal(username), token.getSaml2Response(),
|
||||
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))
|
||||
);
|
||||
} catch (Saml2AuthenticationException e) {
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||
|
||||
/**
|
||||
* Saml2 representation of an {@link AuthenticatedPrincipal}.
|
||||
*
|
||||
* @author Clement Stoquart
|
||||
* @since 5.2.2
|
||||
*/
|
||||
public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal {
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link Saml2AuthenticatedPrincipal}.
|
||||
*
|
||||
* @author Clement Stoquart
|
||||
* @since 5.2.2
|
||||
*/
|
||||
class SimpleSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal, Serializable {
|
||||
|
||||
private final String name;
|
||||
|
||||
SimpleSaml2AuthenticatedPrincipal(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
+26
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import org.hamcrest.BaseMatcher;
|
||||
@@ -346,6 +350,28 @@ public class OpenSamlAuthenticationProviderTests {
|
||||
provider.authenticate(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
|
||||
Response response = response(recipientUri, idpEntityId);
|
||||
Assertion assertion = defaultAssertion();
|
||||
signXmlObject(
|
||||
assertion,
|
||||
assertingPartyCredentials(),
|
||||
recipientEntityId
|
||||
);
|
||||
EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials());
|
||||
response.getEncryptedAssertions().add(encryptedAssertion);
|
||||
token = responseXml(response, idpEntityId);
|
||||
|
||||
Saml2Authentication authentication = (Saml2Authentication) provider.authenticate(token);
|
||||
|
||||
// the following code will throw an exception if authentication isn't serializable
|
||||
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
|
||||
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
|
||||
objectOutputStream.writeObject(authentication);
|
||||
objectOutputStream.flush();
|
||||
}
|
||||
|
||||
private Assertion defaultAssertion() {
|
||||
return assertion(
|
||||
username,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimpleSaml2AuthenticatedPrincipalTests {
|
||||
|
||||
@Test
|
||||
public void createSimpleSaml2AuthenticatedPrincipal() {
|
||||
SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user");
|
||||
|
||||
Assert.assertEquals("user", principal.getName());
|
||||
}
|
||||
}
|
||||
-36
@@ -255,42 +255,6 @@ public class OAuth2LoginApplicationTests {
|
||||
assertThat(errorElement.asText()).contains("authorization_request_not_found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
URL loginPageUrl = page.getBaseURL();
|
||||
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google");
|
||||
|
||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, clientRegistration);
|
||||
assertThat(clientAnchorElement).isNotNull();
|
||||
|
||||
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||
|
||||
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
|
||||
URI.create(response.getResponseHeaderValue("Location"))).build();
|
||||
|
||||
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
|
||||
String code = "auth-code";
|
||||
String state = URLDecoder.decode(params.get(OAuth2ParameterNames.STATE), "UTF-8");
|
||||
String redirectUri = URLDecoder.decode(params.get(OAuth2ParameterNames.REDIRECT_URI), "UTF-8");
|
||||
redirectUri += "-invalid";
|
||||
|
||||
String authorizationResponseUri =
|
||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||
.queryParam(OAuth2ParameterNames.CODE, code)
|
||||
.queryParam(OAuth2ParameterNames.STATE, state)
|
||||
.build().encode().toUriString();
|
||||
|
||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||
|
||||
HtmlElement errorElement = page.getBody().getFirstByXPath("div");
|
||||
assertThat(errorElement).isNotNull();
|
||||
assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
|
||||
}
|
||||
|
||||
private void assertLoginPage(HtmlPage page) {
|
||||
assertThat(page.getTitleText()).isEqualTo("Please sign in");
|
||||
|
||||
|
||||
+2
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.web.authentication.www;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
@@ -276,6 +277,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
|
||||
public void setCredentialsCharset(String credentialsCharset) {
|
||||
Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty");
|
||||
this.credentialsCharset = credentialsCharset;
|
||||
this.authenticationConverter.setCredentialsCharset(Charset.forName(credentialsCharset));
|
||||
}
|
||||
|
||||
protected String getCredentialsCharset(HttpServletRequest httpRequest) {
|
||||
|
||||
+4
-6
@@ -19,11 +19,11 @@ package org.springframework.security.web.server.authentication;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Delegates to a collection of {@link ServerAuthenticationSuccessHandler} implementations.
|
||||
@@ -42,10 +42,8 @@ public class DelegatingServerAuthenticationSuccessHandler implements ServerAuthe
|
||||
@Override
|
||||
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange,
|
||||
Authentication authentication) {
|
||||
List<Mono<Void>> results = new ArrayList<>();
|
||||
for (ServerAuthenticationSuccessHandler delegate : delegates) {
|
||||
results.add(delegate.onAuthenticationSuccess(exchange, authentication));
|
||||
}
|
||||
return Mono.when(results);
|
||||
return Flux.fromIterable(this.delegates)
|
||||
.concatMap(delegate -> delegate.onAuthenticationSuccess(exchange, authentication))
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -21,6 +21,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -48,12 +49,8 @@ public class DelegatingServerLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
||||
List<Mono<Void>> results = new ArrayList<>();
|
||||
for (ServerLogoutHandler delegate : delegates) {
|
||||
if (delegate != null) {
|
||||
results.add(delegate.logout(exchange, authentication));
|
||||
}
|
||||
}
|
||||
return Mono.when(results);
|
||||
return Flux.fromIterable(this.delegates)
|
||||
.concatMap(delegate -> delegate.logout(exchange, authentication))
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-10
@@ -15,13 +15,12 @@
|
||||
*/
|
||||
package org.springframework.security.web.server.header;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Combines multiple {@link ServerHttpHeadersWriter} instances into a single instance.
|
||||
@@ -42,10 +41,8 @@ public class CompositeServerHttpHeadersWriter implements ServerHttpHeadersWriter
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
|
||||
List<Mono<Void>> results = new ArrayList<>();
|
||||
for (ServerHttpHeadersWriter writer : writers) {
|
||||
results.add(writer.writeHttpHeaders(exchange));
|
||||
}
|
||||
return Mono.when(results);
|
||||
return Flux.fromIterable(this.writers)
|
||||
.concatMap(w -> w.writeHttpHeaders(exchange))
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -106,7 +106,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
||||
+ " <body>\n"
|
||||
+ " <div class=\"container\">\n"
|
||||
+ formLogin(queryParams, csrfTokenHtmlInput)
|
||||
+ oauth2LoginLinks(contextPath, this.oauth2AuthenticationUrlToClientName)
|
||||
+ oauth2LoginLinks(queryParams, contextPath, this.oauth2AuthenticationUrlToClientName)
|
||||
+ " </div>\n"
|
||||
+ " </body>\n"
|
||||
+ "</html>";
|
||||
@@ -135,12 +135,14 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
||||
+ " </form>\n";
|
||||
}
|
||||
|
||||
private static String oauth2LoginLinks(String contextPath, Map<String, String> oauth2AuthenticationUrlToClientName) {
|
||||
private static String oauth2LoginLinks(MultiValueMap<String, String> queryParams, String contextPath, Map<String, String> oauth2AuthenticationUrlToClientName) {
|
||||
if (oauth2AuthenticationUrlToClientName.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
boolean isError = queryParams.containsKey("error");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<div class=\"container\"><h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
|
||||
sb.append(createError(isError));
|
||||
sb.append("<table class=\"table table-striped\">\n");
|
||||
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
|
||||
sb.append(" <tr><td>");
|
||||
|
||||
+105
-1
@@ -16,13 +16,14 @@
|
||||
|
||||
package org.springframework.security.web.authentication.www;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.AdditionalMatchers.not;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.junit.After;
|
||||
@@ -40,6 +41,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Tests {@link BasicAuthenticationFilter}.
|
||||
*
|
||||
@@ -320,4 +323,105 @@ public class BasicAuthenticationFilterTests {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenTokenAndFilterCharsetMatchDefaultThenAuthenticated() throws Exception {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü");
|
||||
rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
|
||||
Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1"));
|
||||
|
||||
manager = mock(AuthenticationManager.class);
|
||||
when(manager.authenticate(rodRequest)).thenReturn(rod);
|
||||
when(manager.authenticate(not(eq(rodRequest)))).thenThrow(new BadCredentialsException(""));
|
||||
|
||||
filter = new BasicAuthenticationFilter(manager, new BasicAuthenticationEntryPoint());
|
||||
|
||||
String token = "rod:äöü";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.UTF_8))));
|
||||
request.setServletPath("/some_file.html");
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
// Test
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class));
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod");
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("äöü");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenTokenAndFilterCharsetMatchNonDefaultThenAuthenticated() throws Exception {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü");
|
||||
rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
|
||||
Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1"));
|
||||
|
||||
manager = mock(AuthenticationManager.class);
|
||||
when(manager.authenticate(rodRequest)).thenReturn(rod);
|
||||
when(manager.authenticate(not(eq(rodRequest)))).thenThrow(new BadCredentialsException(""));
|
||||
|
||||
filter = new BasicAuthenticationFilter(manager, new BasicAuthenticationEntryPoint());
|
||||
filter.setCredentialsCharset("ISO-8859-1");
|
||||
|
||||
String token = "rod:äöü";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.ISO_8859_1))));
|
||||
request.setServletPath("/some_file.html");
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
// Test
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class));
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod");
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("äöü");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenTokenAndFilterCharsetDoNotMatchThenUnauthorized() throws Exception {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü");
|
||||
rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
|
||||
Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1"));
|
||||
|
||||
manager = mock(AuthenticationManager.class);
|
||||
when(manager.authenticate(rodRequest)).thenReturn(rod);
|
||||
when(manager.authenticate(not(eq(rodRequest)))).thenThrow(new BadCredentialsException(""));
|
||||
|
||||
filter = new BasicAuthenticationFilter(manager, new BasicAuthenticationEntryPoint());
|
||||
filter.setCredentialsCharset("ISO-8859-1");
|
||||
|
||||
String token = "rod:äöü";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.UTF_8))));
|
||||
request.setServletPath("/some_file.html");
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
// Test
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class));
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+33
-5
@@ -16,10 +16,6 @@
|
||||
|
||||
package org.springframework.security.web.server.authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -27,9 +23,19 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.publisher.PublisherProbe;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @since 5.1
|
||||
@@ -88,4 +94,26 @@ public class DelegatingServerAuthenticationSuccessHandlerTests {
|
||||
this.delegate1Result.assertWasSubscribed();
|
||||
this.delegate2Result.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onAuthenticationSuccessSequential() throws Exception {
|
||||
AtomicBoolean slowDone = new AtomicBoolean();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ServerAuthenticationSuccessHandler slow = (exchange, authentication) ->
|
||||
Mono.delay(Duration.ofMillis(100))
|
||||
.doOnSuccess(__ -> slowDone.set(true))
|
||||
.then();
|
||||
ServerAuthenticationSuccessHandler second = (exchange, authentication) ->
|
||||
Mono.fromRunnable(() -> {
|
||||
latch.countDown();
|
||||
assertThat(slowDone.get())
|
||||
.describedAs("ServerAuthenticationSuccessHandler should be executed sequentially")
|
||||
.isTrue();
|
||||
});
|
||||
DelegatingServerAuthenticationSuccessHandler handler = new DelegatingServerAuthenticationSuccessHandler(slow, second);
|
||||
|
||||
handler.onAuthenticationSuccess(this.exchange, this.authentication).block();
|
||||
|
||||
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.web.server.authentication.logout;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -28,9 +29,14 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.publisher.PublisherProbe;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* @author Eric Deandrea
|
||||
@@ -98,4 +104,26 @@ public class DelegatingServerLogoutHandlerTests {
|
||||
this.delegate1Result.assertWasSubscribed();
|
||||
this.delegate2Result.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutSequential() throws Exception {
|
||||
AtomicBoolean slowDone = new AtomicBoolean();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ServerLogoutHandler slow = (exchange, authentication) ->
|
||||
Mono.delay(Duration.ofMillis(100))
|
||||
.doOnSuccess(__ -> slowDone.set(true))
|
||||
.then();
|
||||
ServerLogoutHandler second = (exchange, authentication) ->
|
||||
Mono.fromRunnable(() -> {
|
||||
latch.countDown();
|
||||
assertThat(slowDone.get())
|
||||
.describedAs("ServerLogoutHandler should be executed sequentially")
|
||||
.isTrue();
|
||||
});
|
||||
DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(slow, second);
|
||||
|
||||
handler.logout(this.exchange, this.authentication).block();
|
||||
|
||||
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
+32
-10
@@ -15,11 +15,6 @@
|
||||
*/
|
||||
package org.springframework.security.web.server.header;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -28,10 +23,19 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
@@ -55,7 +59,6 @@ public class CompositeServerHttpHeadersWriterTests {
|
||||
@Test
|
||||
public void writeHttpHeadersWhenErrorNoErrorThenError() {
|
||||
when(writer1.writeHttpHeaders(exchange)).thenReturn(Mono.error(new RuntimeException()));
|
||||
when(writer2.writeHttpHeaders(exchange)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = writer.writeHttpHeaders(exchange);
|
||||
|
||||
@@ -64,13 +67,11 @@ public class CompositeServerHttpHeadersWriterTests {
|
||||
.verify();
|
||||
|
||||
verify(writer1).writeHttpHeaders(exchange);
|
||||
verify(writer2).writeHttpHeaders(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeHttpHeadersWhenErrorErrorThenError() {
|
||||
when(writer1.writeHttpHeaders(exchange)).thenReturn(Mono.error(new RuntimeException()));
|
||||
when(writer2.writeHttpHeaders(exchange)).thenReturn(Mono.error(new RuntimeException()));
|
||||
|
||||
Mono<Void> result = writer.writeHttpHeaders(exchange);
|
||||
|
||||
@@ -79,7 +80,6 @@ public class CompositeServerHttpHeadersWriterTests {
|
||||
.verify();
|
||||
|
||||
verify(writer1).writeHttpHeaders(exchange);
|
||||
verify(writer2).writeHttpHeaders(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -96,4 +96,26 @@ public class CompositeServerHttpHeadersWriterTests {
|
||||
verify(writer1).writeHttpHeaders(exchange);
|
||||
verify(writer2).writeHttpHeaders(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeHttpHeadersSequential() throws Exception {
|
||||
AtomicBoolean slowDone = new AtomicBoolean();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ServerHttpHeadersWriter slow = exchange ->
|
||||
Mono.delay(Duration.ofMillis(100))
|
||||
.doOnSuccess(__ -> slowDone.set(true))
|
||||
.then();
|
||||
ServerHttpHeadersWriter second = exchange ->
|
||||
Mono.fromRunnable(() -> {
|
||||
latch.countDown();
|
||||
assertThat(slowDone.get())
|
||||
.describedAs("ServerLogoutHandler should be executed sequentially")
|
||||
.isTrue();
|
||||
});
|
||||
CompositeServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(slow, second);
|
||||
|
||||
writer.writeHttpHeaders(this.exchange).block();
|
||||
|
||||
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ public class HttpHeaderWriterWebFilterTests {
|
||||
|
||||
verify(writer, never()).writeHttpHeaders(any());
|
||||
|
||||
result.getExchange().getResponse().setComplete();
|
||||
result.getExchange().getResponse().setComplete().block();
|
||||
|
||||
verify(writer).writeHttpHeaders(any());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user