1
0
mirror of synced 2026-07-08 12:20:02 +00:00

Compare commits

...

39 Commits

Author SHA1 Message Date
Joe Grandja 9a2b71d931 Release 5.2.2.RELEASE 2020-02-05 10:56:00 -05:00
Josh Cummings c4ccc96655 Polish Error Messages for OpaqueTokenIntrospectors 2020-02-05 07:16:37 -07:00
Joe Grandja 6c310213a8 Update to Spring Boot 2.2.4
Fixes gh-7909
2020-02-04 15:07:16 -05:00
Joe Grandja a5b6b9a398 Update to org.slf4j 1.7.30
Fixes gh-7908
2020-02-04 15:04:46 -05:00
Joe Grandja 9e6910273c Update to org.powermock 2.0.5
Fixes gh-7907
2020-02-04 14:56:28 -05:00
Joe Grandja ea809b01a6 Update to hibernate-validator 6.1.2.Final
Fixes gh-7906
2020-02-04 14:53:08 -05:00
Joe Grandja 8054239a12 Update to hibernate-entitymanager 5.4.10.Final
Fixes gh-7905
2020-02-04 14:51:05 -05:00
Joe Grandja 46486194c2 Update to org.aspectj 1.9.5
Fixes gh-7904
2020-02-04 14:44:05 -05:00
Joe Grandja 00b08bc725 Update to httpclient 4.5.11
Fixes gh-7903
2020-02-04 14:39:27 -05:00
Joe Grandja 6e0fbfcccd Update to commons-codec 1.14
Fixes gh-7899
2020-02-04 14:31:31 -05:00
Joe Grandja 87ea083520 Update to com.squareup.okhttp3 3.14.6
Fixes gh-7898
2020-02-04 14:24:11 -05:00
Joe Grandja 9db3f51f2a Update to Jackson 2.10.2
Fixes gh-7897
2020-02-04 14:06:11 -05:00
Joe Grandja 3cc4a945c6 Update to Reactor Dysprosium SR4
Fixes gh-7896
2020-02-04 14:03:06 -05:00
Joe Grandja dbc43fb47d Update to Spring Data Moore SR3
Fixes gh-7895
2020-02-04 14:02:57 -05:00
Joe Grandja ce6a0368bd Update to Spring Framework 5.2.3
Fixes gh-7894
2020-02-04 13:38:17 -05:00
Eleftheria Stein 9dd3dfe718 Fix requiresAuthenticationMatcher not being used
The custom server requiresAuthenticationMatcher was not always picked up

Fixes: gh-7863
2020-01-27 16:56:59 +01:00
Eleftheria Stein edb6cd3729 Fix authenticationFailureHandler not being used
The custom server authenticationFailureHandler was not always picked up

Fixes: gh-7782
2020-01-27 13:52:01 +01:00
Peter Keller 2dbedf7af5 Set charset of BasicAuthenticationFilter converter
Allow BasicAuthenticationFilter to pick up the given credentials charset.

Fixes: gh-7835
2020-01-23 16:24:03 +01:00
Eleftheria Stein 630eb10704 Load LDIF file from classpath in unboundId mode
Fixes: gh-7833
2020-01-21 17:12:18 +01:00
Eleftheria Stein f4d4c08329 Fix LDIF file example in LDAP docs
Fixes: gh-7832
2020-01-20 11:32:53 +01:00
Johannes Edmeier cc956a66df Don't cache requests with Accept: text/event-stream by default.
The eventstream requests is typically not directly invoked by the browser.
And even more unfortunately the Browser-Api doesn't allow the set additional headers as `XMLHttpRequest`..
2020-01-17 10:37:34 -08:00
Rob Winch 29182abb34 Fix HttpHeaderWriterWebFilterTests
Ensure setComplete() is subscribed to
2020-01-10 08:46:47 -06:00
Filip Hanik b754a3d635 Use the custom ServerRequestCache that the user configures
on for the default authentication entry point and authentication
success handler

Fixes gh-7721

https://github.com/spring-projects/spring-security/issues/7721

Set RequestCache on the Oauth2LoginSpec default authentication success handler

import static ReflectionTestUtils.getField

Feedback incorporated per

https://github.com/spring-projects/spring-security/pull/7734#pullrequestreview-332150359
2019-12-18 08:44:27 -08:00
Eleftheria Stein 0d24e2b8cf Fix WebFlux logout disabling
Fixes: gh-7682
2019-12-13 11:53:20 +01:00
Rob Winch b00999deed Docs ServerRSocketFactoryCustomizer->ServerRSocketFactoryProcessor
The documentation incorrectly used ServerRSocketFactoryCustomizer which
was renamed to ServerRSocketFactoryProcessor. The docs now use the correct
class name

Fixes gh-7737
2019-12-12 15:30:56 -06:00
Eleftheria Stein 59ca2ddf65 Polish SAML2 principal classes
Update @since

Issue: gh-7681
2019-12-12 20:27:24 +01:00
Clement Stoquart 0782228914 fix: make Saml2Authentication serializable 2019-12-12 20:25:26 +01:00
Rob Winch 29eb8b9177 CompositeServerHttpHeadersWriter Executes Sequentially
Fixes gh-7731
2019-12-12 11:28:23 -06:00
Rob Winch bd6ff1f319 DelegatingServerAuthenticationSuccessHandler Executes Sequentially
Fixes gh-7728
2019-12-12 08:33:14 -06:00
Rob Winch 6db7b457b7 DelegatingServerLogoutHandler Executes Sequentially
Fixes gh-7723
2019-12-11 15:39:56 -06:00
Phil Clay 840d3aa986 Polish #7589
Rename OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager to AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.

Handle empty mono returned from contextAttributesMapper.

Handle empty map returned from contextAttributesMapper.

Fix DefaultContextAttributesMapper so that it doesn't access ServerWebExchange.

Fix unit tests so that they pass.

Use StepVerifier in unit tests, rather than .subscribe().

Fixes gh-7569
2019-12-10 14:37:34 -05:00
Ankur Pathak 4c5c4f6cce Reactive Implementation of AuthorizedClientServiceOAuth2AuthorizedClientManager
ReactiveOAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager is reactive
version of AuthorizedClientServiceOAuth2AuthorizedClientManager

Fixes: gh-7569
2019-12-10 14:37:25 -05:00
Joe Grandja 148b570a98 Remove redundant validation for redirect-uri
Fixes gh-7706
2019-12-06 12:01:19 -05:00
Joe Grandja 752d5f29aa Display general error message when WebFlux oauth2Login() fails
Issue gh-5562 gh-6484
2019-12-05 20:12:38 -05:00
Joe Grandja e4aa3be4c5 WebFlux oauth2Login() redirects on failed authentication
Fixes gh-5562 gh-6484
2019-12-05 20:12:09 -05:00
Alexey Nesterov 0babe7d930 Correctly configure authorization requests repository for OAuth2 login
To use custom ServerAuthorizationRequestRepository both OAuth2AuthorizationRequestRedirectWebFilter and
OAuth2LoginAuthenticationWebFilter should use the same repo provided in the configuration. Currently the former filter is
correctly configured, but the latter always uses default, WebSession based repository. So authorization code created
before redirect to authorization endpoint will never be found to complete OAuth2 login when custom
ServerAuthorizationRequestRepository is used.

This change also makes OAuth2Client and OAuth2Login authentication converters consistent.

Fixes gh-7675
2019-11-29 13:58:27 -05:00
Joe Grandja b905cb8aaa Polish OAuth2AuthorizedClientArgumentResolver 2019-11-28 10:31:29 -05:00
Joe Grandja 19c2209a12 ServerOAuth2AuthorizedClientExchangeFilterFunction works with UnAuthenticatedServerOAuth2AuthorizedClientRepository
Fixes gh-7544
2019-11-28 10:31:18 -05:00
Joe Grandja 18f48e4a16 DefaultReactiveOAuth2AuthorizedClientManager requires non-null serverWebExchange
Issue gh-7544
2019-11-28 10:31:06 -05:00
50 changed files with 1440 additions and 242 deletions
@@ -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);
}
@@ -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;
}
@@ -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
}
}
}
@@ -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 {
@@ -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
@@ -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
@@ -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();
}
}
@@ -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 {
@@ -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
View File
@@ -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
+23 -23
View File
@@ -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'
@@ -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));
}
}
@@ -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,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);
}
}
}
@@ -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(
@@ -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());
@@ -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() {
@@ -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;
}
}
}
@@ -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() {
@@ -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,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,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,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();
@@ -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);
@@ -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")
@@ -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);
}
@@ -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);
@@ -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");
}
}
@@ -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
@@ -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
@@ -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) {
@@ -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 {
}
@@ -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;
}
}
@@ -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,
@@ -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());
}
}
@@ -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");
@@ -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) {
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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>");
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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());
}