1
0
mirror of synced 2026-08-06 10:18:52 +00:00

Compare commits

...

47 Commits

Author SHA1 Message Date
Joe Grandja 84f5713d26 Release 5.1.8.RELEASE 2020-02-05 09:45:19 -05:00
Joe Grandja 137c211667 Update to Spring Boot 2.1.12
Fixes gh-7923
2020-02-04 16:56:11 -05:00
Joe Grandja 4e4785c6c2 Update to org.slf4j 1.7.30
Fixes gh-7922
2020-02-04 16:55:11 -05:00
Joe Grandja ec0e03cff2 Update to org.powermock 2.0.5
Fixes gh-7921
2020-02-04 16:43:49 -05:00
Joe Grandja e3a50c5039 Update to hibernate-validator 6.0.18.Final
Fixes gh-7920
2020-02-04 16:39:50 -05:00
Joe Grandja b2476915e2 Update to hibernate-entitymanager 5.3.15.Final
Fixes gh-7919
2020-02-04 16:37:35 -05:00
Joe Grandja 90b1c877f1 Update to org.bouncycastle:bcpkix-jdk15on 1.64
Fixes gh-7918
2020-02-04 16:35:24 -05:00
Joe Grandja 57e69156c5 Update to org.aspectj 1.9.5
Fixes gh-7917
2020-02-04 16:32:43 -05:00
Joe Grandja 4c3ac0a1a4 Update to httpclient 4.5.11
Fixes gh-7916
2020-02-04 16:30:23 -05:00
Joe Grandja d9521cb880 Update to com.squareup.okhttp3 3.12.8
Fixes gh-7915
2020-02-04 16:27:09 -05:00
Joe Grandja 7d7cdbb09c Update to Jackson 2.9.10
Fixes gh-7914
2020-02-04 16:23:13 -05:00
Joe Grandja c2129d3812 Update to Reactor Californium-SR15
Fixes gh-7913
2020-02-04 16:16:33 -05:00
Joe Grandja 097c4bf2a1 Update to Spring Data Lovelace SR15
Fixes gh-7912
2020-02-04 16:14:57 -05:00
Joe Grandja b8938c98d3 Update to Spring Framework 5.1.13
Fixes gh-7911
2020-02-04 16:12:24 -05:00
Eleftheria Stein 0520810a0d Load LDIF file from classpath in unboundId mode
Fixes: gh-7833
2020-01-21 17:17:18 +01:00
Filip Hanik 8711ba5a30 Build using openjdk8
Fixes gh-7169

[closes #7169]
2020-01-13 10:32:10 -07:00
Rob Winch 0f8927b500 CompositeServerHttpHeadersWriter Executes Sequentially
Fixes gh-7731
2019-12-12 12:20:09 -06:00
Rob Winch 1136660518 DelegatingServerAuthenticationSuccessHandler Executes Sequentially
Fixes gh-7728
2019-12-12 09:31:43 -06:00
Rob Winch c395da3e04 DelegatingServerLogoutHandler Executes Sequentially
Fixes gh-7723
2019-12-12 07:48:58 -06:00
Joe Grandja 798c48eee3 Remove redundant validation for redirect-uri
Fixes gh-7706
2019-12-06 12:09:09 -05:00
Joe Grandja b7f1bdae64 Display general error message when WebFlux oauth2Login() fails
Issue gh-5562 gh-6484
2019-12-05 20:45:18 -05:00
Joe Grandja 459e8f1a11 WebFlux oauth2Login() redirects on failed authentication
Fixes gh-5562 gh-6484
2019-12-05 20:42:23 -05:00
Warren Bailey 450a20add4 When expired retrieve new Client Credentials token.
Once client credentials access token has expired retrieve a new token from the OAuth2 authorization server.
These tokens can't be refreshed because they do not have a refresh token associated with. This is standard behaviour for Oauth 2 client credentails

Fixes gh-5893
2019-11-28 16:02:06 -05:00
Rob Winch f3f84e1aff Next Development Version 2019-11-04 13:58:52 -06:00
Rob Winch 4a9d13dc5c Release 5.1.7.RELEASE 2019-11-04 13:56:38 -06:00
Rob Winch 62529278b5 Update to Spring Data Lovelace-SR12
Fixes gh-7617
2019-11-04 13:04:01 -06:00
Rob Winch dcb3b6940f Update to Spring 5.1.11.RELEASE
Fixes gh-7616
2019-11-04 13:03:48 -06:00
Rob Winch a82047e61b Update Californium-SR13
Fixes gh-7615
2019-11-04 13:03:36 -06:00
Rob Winch 6c54e5f278 Update to Spring Boot 2.1.9.RELEASE
Fixes gh-7614
2019-11-04 13:03:14 -06:00
Josh Cummings 8589580aa2 CookieServerCsrfTokenRepositoryTests Leading Dot
ResponseCookie removed support for having a leading dot in the cookie
domain.

Fixes gh-7500
2019-09-30 08:52:42 -06:00
Joe Grandja 1749c8df9c OAuth2AuthorizationCodeGrantWebFilter matches on registered redirect-uri
Fixes gh-7036
2019-09-24 15:50:19 -04:00
Roman Matiushchenko a5391b629e Fix NPE in RequestContextSubscriber
RequestContextSubscriber could cause NPE if Mono/Flux.subscribe()
was invoked outside of Web Context.
In addition it replaced source Context with its own without respect
to old data.
Now Request Context Data is Propagated within holder class and
it is added to existing reactor Context if Holder is not empty.

Fixes gh-7228
2019-09-08 20:35:09 -04:00
Andreas Kluth 57f3c76801 Remove OAuth2AuthorizationRequest when a distributed session is used
Dirties the WebSession by putting the amended AUTHORIZATION_REQUEST map into
the WebSession even it was already in the map. This causes common SessionRepository
implementations like Redis to persist the updated attribute.

Fixes gh-7327

Author: Andreas Kluth <mail@andreaskluth.net>
2019-09-05 10:04:14 -04:00
Rob Winch f8f1e9a3d4 Use ConcurrentHashMap in InMemoryReactiveClientRegistrationRepository
Fixes gh-7299
2019-09-04 19:21:22 -05:00
Bouke Nijhuis bf78e43403 add media type jwk-set+json to accept header
Fixes gh-7290
2019-09-03 14:39:18 -04:00
Angel Aguilera 3599ad7547 use simple link instead of 'pass' macro 2019-08-29 06:23:26 -05:00
Angel Aguilera 143e3ac249 fix footnotes markup
Fixes #3180
2019-08-29 06:23:26 -05:00
Josh Cummings 362356dea5 Add WebSecurityConfigurerAdapter Doc Detail
Fixes gh-6809
2019-08-22 17:58:14 -06:00
Daniel Wegener f28fe2d501 Add OnCommittedResponseWrapper.setContentLengthLong
Add setContentLengthLong tracking to OnCommittedResponseWrapper in
order to detect commits on servlets that use setContentLengthLong to
announce the entity size they are about to write (as used in the
Apache Tomcat's DefaultServlet).

Fixes gh-7261
2019-08-22 12:19:33 -06:00
Rob Winch 5114190cb6 Fix WebClient Memory Leaks
WebClient exchange requires that the body is consumed. Before this commit
there were places where an Exception was thrown without consuming the body
if the status was not successful. There was also the potential for the
statusCode invocation to throw an Exception of the status code was not
defined which would cause a leak.

This commit ensures that before the Exception is thrown the body is
consumed. It also uses the http status in a way that will ensure an
Exception is not thrown.

Fixes gh-7293
2019-08-21 12:46:54 -05:00
Rob Winch 93d1c7f59f Fix JDK 10+
Issue gh-7265
2019-08-16 11:15:01 -05:00
Rob Winch 2c128e85fa SecurityMockMvcConfigurer Honors Filter Order
Fixes gh-7265
2019-08-16 09:41:16 -05:00
Eleftheria Stein a17b75e862 Expire as many sessions as exceed maximum allowed
Fixes: gh-7166
2019-08-15 09:49:08 -05:00
Jeffrey Morlan 178a5e0819 Fix race condition in SessionRegistryImpl
Adding/removing sessions from principals wasn't atomic. If one thread
removed the last session from a principal while another thread added a
new one, the addition could be lost.

Fixes gh-3189
2019-08-06 13:46:08 -05:00
kostya05983 7542d59364 Fix docs typo WebSecurityConfigurationAdapter->WebSecurityConfigurerAdapter
Fixes gh-7026
2019-08-06 13:08:19 -05:00
Josh Cummings 83b7d7d461 Update to 5.1.7.BUILD-SNAPSHOT 2019-08-05 17:47:15 -06:00
Josh Cummings 17b48b0b60 Next Development Version 2019-08-05 17:43:50 -06:00
56 changed files with 4178 additions and 374 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
language: java
jdk:
- oraclejdk8
- openjdk8
os:
- linux
@@ -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.
@@ -53,7 +53,6 @@ import org.springframework.security.authorization.AuthorityReactiveAuthorization
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager;
@@ -89,7 +88,6 @@ import org.springframework.security.web.server.DelegatingServerAuthenticationEnt
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
@@ -619,16 +617,8 @@ public class ServerHttpSecurity {
AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
authenticationFilter.setRequiresAuthenticationMatcher(createAttemptAuthenticationRequestMatcher());
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
RedirectServerAuthenticationSuccessHandler redirectHandler = new RedirectServerAuthenticationSuccessHandler();
authenticationFilter.setAuthenticationSuccessHandler(redirectHandler);
authenticationFilter.setAuthenticationFailureHandler(new ServerAuthenticationFailureHandler() {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,
AuthenticationException exception) {
return Mono.error(exception);
}
});
authenticationFilter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler());
authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error"));
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
@@ -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.
@@ -34,11 +34,17 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.web.server.SecurityWebFilterChain;
@@ -69,8 +75,11 @@ public class OAuth2ClientSpecTests {
private ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
private ApplicationContext context;
@Autowired
public void setApplicationContext(ApplicationContext context) {
this.context = context;
this.client = WebTestClient.bindToApplicationContext(context).build();
}
@@ -140,19 +149,40 @@ public class OAuth2ClientSpecTests {
ServerAuthenticationConverter converter = config.authenticationConverter;
ReactiveAuthenticationManager manager = config.manager;
ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
new WebSessionOAuth2ServerAuthorizationRequestRepository();
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
.redirectUri("/authorize/oauth2/code/registration-id")
.build();
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
.redirectUri("/authorize/oauth2/code/registration-id")
.build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
OAuth2AuthorizationCodeAuthenticationToken result = new OAuth2AuthorizationCodeAuthenticationToken(this.registration, exchange, accessToken);
OAuth2AuthorizationCodeAuthenticationToken result = new OAuth2AuthorizationCodeAuthenticationToken(
this.registration, authorizationExchange, accessToken);
when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
when(manager.authenticate(any())).thenReturn(Mono.just(result));
this.client.get()
.uri("/authorize/oauth2/code/registration-id")
.exchange()
.expectStatus().is3xxRedirection();
WebTestClient client = WebTestClient.bindToApplicationContext(this.context)
.webFilter((exchange, chain) ->
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange)
.then(chain.filter(exchange).then(Mono.empty()))
)
.build();
client.get()
.uri(uriBuilder ->
uriBuilder.path("/authorize/oauth2/code/registration-id")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build())
.exchange()
.expectStatus().is3xxRedirection();
verify(converter).convert(any());
verify(manager).authenticate(any());
@@ -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.
@@ -34,13 +34,26 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -54,6 +67,9 @@ import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
/**
* @author Rob Winch
* @since 5.1
@@ -72,6 +88,12 @@ public class OAuth2LoginTests {
.clientSecret("secret")
.build();
private static ClientRegistration google = CommonOAuth2Provider.GOOGLE
.getBuilder("google")
.clientId("client")
.clientSecret("secret")
.build();
@Test
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
@@ -97,11 +119,6 @@ public class OAuth2LoginTests {
static class OAuth2LoginWithMulitpleClientRegistrations {
@Bean
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration google = CommonOAuth2Provider.GOOGLE
.getBuilder("google")
.clientId("client")
.clientSecret("secret")
.build();
return new InMemoryReactiveClientRegistrationRepository(github, google);
}
}
@@ -182,6 +199,78 @@ public class OAuth2LoginTests {
}
}
// gh-5562
@Test
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.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 = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "openid", Instant.now(), Instant.now().plus(Duration.ofDays(1)));
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");
}
@Configuration
static class OAuth2LoginWithCustomBeansConfig {
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient =
mock(ReactiveOAuth2AccessTokenResponseClient.class);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
@Bean
public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2Login()
.authenticationConverter(authenticationConverter)
.authenticationManager(authenticationManager());
return http.build();
// @formatter:on
}
private ReactiveAuthenticationManager authenticationManager() {
OidcAuthorizationCodeReactiveAuthenticationManager oidc =
new OidcAuthorizationCodeReactiveAuthenticationManager(tokenResponseClient, userService);
return oidc;
}
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
return tokenResponseClient;
}
}
static class GitHubWebFilter implements WebFilter {
@Override
@@ -132,13 +132,18 @@ public class SessionRegistryImpl implements SessionRegistry,
sessionIds.put(sessionId,
new SessionInformation(principal, sessionId, new Date()));
Set<String> sessionsUsedByPrincipal = principals.computeIfAbsent(principal, key -> new CopyOnWriteArraySet<>());
sessionsUsedByPrincipal.add(sessionId);
principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
}
sessionsUsedByPrincipal.add(sessionId);
if (logger.isTraceEnabled()) {
logger.trace("Sessions used by '" + principal + "' : "
+ sessionsUsedByPrincipal);
}
if (logger.isTraceEnabled()) {
logger.trace("Sessions used by '" + principal + "' : "
+ sessionsUsedByPrincipal);
}
return sessionsUsedByPrincipal;
});
}
public void removeSessionInformation(String sessionId) {
@@ -157,32 +162,29 @@ public class SessionRegistryImpl implements SessionRegistry,
sessionIds.remove(sessionId);
Set<String> sessionsUsedByPrincipal = principals.get(info.getPrincipal());
if (sessionsUsedByPrincipal == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Removing session " + sessionId
+ " from principal's set of registered sessions");
}
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
// No need to keep object in principals Map anymore
principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
if (logger.isDebugEnabled()) {
logger.debug("Removing principal " + info.getPrincipal()
+ " from registry");
logger.debug("Removing session " + sessionId
+ " from principal's set of registered sessions");
}
principals.remove(info.getPrincipal());
}
if (logger.isTraceEnabled()) {
logger.trace("Sessions used by '" + info.getPrincipal() + "' : "
+ sessionsUsedByPrincipal);
}
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
// No need to keep object in principals Map anymore
if (logger.isDebugEnabled()) {
logger.debug("Removing principal " + info.getPrincipal()
+ " from registry");
}
sessionsUsedByPrincipal = null;
}
if (logger.isTraceEnabled()) {
logger.trace("Sessions used by '" + info.getPrincipal() + "' : "
+ sessionsUsedByPrincipal);
}
return sessionsUsedByPrincipal;
});
}
}
+2873
View File
File diff suppressed because it is too large Load Diff
@@ -258,7 +258,7 @@ Or you might work for a company that has a legacy proprietary authentication sys
In situations like this it's quite easy to get Spring Security to work, and still provide authorization capabilities.
All you need to do is write a filter (or equivalent) that reads the third-party user information from a location, build a Spring Security-specific `Authentication` object, and put it into the `SecurityContextHolder`.
In this case you also need to think about things which are normally taken care of automatically by the built-in authentication infrastructure.
For example, you might need to pre-emptively create an HTTP session to <<tech-intro-sec-context-persistence,cache the context between requests>>, before you write the response to the client footnote:[It isn't possible to create a session once the response has been committed.
For example, you might need to pre-emptively create an HTTP session to <<tech-intro-sec-context-persistence,cache the context between requests>>, before you write the response to the client footnote:[It isn't possible to create a session once the response has been committed.].
If you're wondering how the `AuthenticationManager` is implemented in a real world example, we'll look at that in the <<core-services-authentication-manager,core services chapter>>.
@@ -25,10 +25,10 @@ import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;
@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() throws Exception {
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
return manager;
@@ -131,7 +131,10 @@ public class MvcWebApplicationInitializer extends
== HttpSecurity
Thus far our <<jc-hello-wsca,WebSecurityConfig>> only contains information about how to authenticate our users.
How does Spring Security know that we want to require all users to be authenticated? How does Spring Security know we want to support form based authentication? The reason for this is that the `WebSecurityConfigurerAdapter` provides a default configuration in the `configure(HttpSecurity http)` method that looks like:
How does Spring Security know that we want to require all users to be authenticated?
How does Spring Security know we want to support form based authentication?
Actually, there is an configuration class that is being invoked behind the scenes called `WebSecurityConfigurerAdapter`.
It has a method called `configure` with the following default implementation:
[source,java]
----
@@ -172,9 +175,17 @@ I want to configure authorized requests __and__ configure form login __and__ con
You might be wondering where the login form came from when you were prompted to log in, since we made no mention of any HTML files or JSPs.
Since Spring Security's default configuration does not explicitly set a URL for the login page, Spring Security generates one automatically, based on the features that are enabled and using standard values for the URL which processes the submitted login, the default target URL the user will be sent to after logging in and so on.
While the automatically generated log in page is convenient to get up and running quickly, most applications will want to provide their own log in page.
To do so we can update our configuration as seen below:
While the automatically generated log in page is convenient to get up and running quickly, most applications will want to provide their own login page.
When we want to change the default configuration, we can customize the `WebSecurityConfigurerAdapter` that we mentioned earlier by extending it like so:
[source,java]
----
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
----
And then override the `configure` method as seen below:
[source,java]
----
@@ -1055,7 +1066,7 @@ public BCryptPasswordEncoder passwordEncoder() {
== Multiple HttpSecurity
We can configure multiple HttpSecurity instances just as we can have multiple `<http>` blocks.
The key is to extend the `WebSecurityConfigurationAdapter` multiple times.
The key is to extend the `WebSecurityConfigurerAdapter` multiple times.
For example, the following is an example of having a different configuration for URL's that start with `/api/`.
[source,java]
@@ -133,7 +133,7 @@ With the default configuration, this is typically a comma-separated list of role
The prefix "ROLE_" is a marker which indicates that a simple comparison with the user's authorities should be made.
In other words, a normal role-based check should be used.
Access-control in Spring Security is not limited to the use of simple roles (hence the use of the prefix to differentiate between different types of security attributes).
We'll see later how the interpretation can vary footnote:[The interpretation of the comma-separated values in the `access` attribute depends on the implementation of the pass:specialcharacters,macros[<<ns-access-manager,AccessDecisionManager>>] which is used.
We'll see later how the interpretation can vary footnote:[The interpretation of the comma-separated values in the `access` attribute depends on the implementation of the <<ns-access-manager,AccessDecisionManager>> which is used.].
In Spring Security 3.0, the attribute can also be populated with an pass:specialcharacters,macros[<<el-access,EL expression>>].
+2 -2
View File
@@ -1,3 +1,3 @@
gaeVersion=1.9.71
springBootVersion=2.1.6.RELEASE
version=5.1.6.RELEASE
springBootVersion=2.1.12.RELEASE
version=5.1.8.RELEASE
+26 -26
View File
@@ -1,13 +1,13 @@
if (!project.hasProperty('reactorVersion')) {
ext.reactorVersion = 'Californium-SR10'
ext.reactorVersion = 'Californium-SR15'
}
if (!project.hasProperty('springVersion')) {
ext.springVersion = '5.1.9.RELEASE'
ext.springVersion = '5.1.13.RELEASE'
}
if (!project.hasProperty('springDataVersion')) {
ext.springDataVersion = 'Lovelace-SR9'
ext.springDataVersion = 'Lovelace-SR15'
}
dependencyManagement {
@@ -18,16 +18,16 @@ dependencyManagement {
}
dependencies {
dependency 'cglib:cglib-nodep:3.2.12'
dependency 'com.squareup.okhttp3:mockwebserver:3.12.3'
dependency 'com.squareup.okhttp3:mockwebserver:3.12.8'
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.2'
dependency 'org.powermock:powermock-api-support:2.0.2'
dependency 'org.powermock:powermock-core:2.0.2'
dependency 'org.powermock:powermock-module-junit4-common:2.0.2'
dependency 'org.powermock:powermock-module-junit4:2.0.2'
dependency 'org.powermock:powermock-reflect:2.0.2'
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.3'
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
@@ -40,9 +40,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.9.9'
dependency 'com.fasterxml.jackson.core:jackson-core:2.9.9'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.10'
dependency 'com.fasterxml.jackson.core:jackson-core:2.9.10'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.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:1.9.76'
@@ -56,7 +56,7 @@ dependencyManagement {
dependency 'com.nimbusds:lang-tag:1.4.3'
dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2'
dependency 'com.nimbusds:oauth2-oidc-sdk:6.0'
dependency 'com.squareup.okhttp3:okhttp:3.12.2'
dependency 'com.squareup.okhttp3:okhttp:3.12.8'
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'
@@ -127,7 +127,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.9'
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'
@@ -140,12 +140,12 @@ dependencyManagement {
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:8.0.44'
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
dependency 'org.apache.tomcat:tomcat-annotations-api:8.5.23'
dependency 'org.aspectj:aspectjrt:1.9.2'
dependency 'org.aspectj:aspectjtools:1.9.2'
dependency 'org.aspectj:aspectjweaver:1.9.2'
dependency 'org.aspectj:aspectjrt:1.9.5'
dependency 'org.aspectj:aspectjtools:1.9.5'
dependency 'org.aspectj:aspectjweaver:1.9.5'
dependency 'org.assertj:assertj-core:3.11.1'
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
dependency 'org.bouncycastle:bcpkix-jdk15on:1.62'
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
dependency 'org.bouncycastle:bcprov-jdk15on:1.58'
dependency 'org.codehaus.groovy:groovy-all:2.4.17'
dependency 'org.codehaus.groovy:groovy-json:2.4.17'
@@ -171,8 +171,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.3.10.Final'
dependency 'org.hibernate:hibernate-validator:6.0.17.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final'
dependency 'org.hibernate:hibernate-validator:6.0.18.Final'
dependency 'org.hsqldb:hsqldb:2.4.1'
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
dependency 'org.javassist:javassist:3.22.0-CR2'
@@ -189,11 +189,11 @@ dependencyManagement {
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
dependency 'org.skyscreamer:jsonassert:1.5.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.26'
dependency 'org.slf4j:jul-to-slf4j:1.7.26'
dependency 'org.slf4j:log4j-over-slf4j:1.7.26'
dependency 'org.slf4j:slf4j-api:1.7.26'
dependency 'org.slf4j:slf4j-nop:1.7.26'
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.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
@@ -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));
}
}
@@ -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);
}
}
}
@@ -15,7 +15,10 @@
*/
package org.springframework.security.oauth2.client.endpoint;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -65,15 +68,18 @@ public class WebClientReactiveClientCredentialsTokenResponseClient implements Re
.headers(headers(clientRegistration))
.body(body)
.exchange()
.flatMap(response ->{
if (!response.statusCode().is2xxSuccessful()){
.flatMap(response -> {
HttpStatus status = HttpStatus.resolve(response.rawStatusCode());
if (status == null || !status.is2xxSuccessful()) {
// extract the contents of this into a method named oauth2AccessTokenResponse but has an argument for the response
throw WebClientResponseException.create(response.rawStatusCode(),
return response.bodyToFlux(DataBuffer.class)
.map(DataBufferUtils::release)
.then(Mono.error(WebClientResponseException.create(response.rawStatusCode(),
"Cannot get token, expected 2xx HTTP Status code",
null,
null,
null
);
)));
}
return response.body(oauth2AccessTokenResponse()); })
.map(response -> {
@@ -73,7 +73,6 @@ import org.springframework.util.StringUtils;
*/
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 MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
@@ -127,11 +126,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(
@@ -76,7 +76,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 MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
@@ -127,11 +126,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());
@@ -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.
@@ -15,15 +15,22 @@
*/
package org.springframework.security.oauth2.client.registration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
/**
* A Reactive {@link ClientRegistrationRepository} that stores {@link ClientRegistration}(s) in-memory.
*
* @author Rob Winch
* @author Ebert Toribio
* @since 5.1
* @see ClientRegistrationRepository
* @see ClientRegistration
@@ -31,7 +38,7 @@ import reactor.core.publisher.Mono;
public final class InMemoryReactiveClientRegistrationRepository
implements ReactiveClientRegistrationRepository, Iterable<ClientRegistration> {
private final InMemoryClientRegistrationRepository delegate;
private final Map<String, ClientRegistration> clientIdToClientRegistration;
/**
* Constructs an {@code InMemoryReactiveClientRegistrationRepository} using the provided parameters.
@@ -39,7 +46,12 @@ public final class InMemoryReactiveClientRegistrationRepository
* @param registrations the client registration(s)
*/
public InMemoryReactiveClientRegistrationRepository(ClientRegistration... registrations) {
this.delegate = new InMemoryClientRegistrationRepository(registrations);
this(toList(registrations));
}
private static List<ClientRegistration> toList(ClientRegistration... registrations) {
Assert.notEmpty(registrations, "registrations cannot be null or empty");
return Arrays.asList(registrations);
}
/**
@@ -48,12 +60,12 @@ public final class InMemoryReactiveClientRegistrationRepository
* @param registrations the client registration(s)
*/
public InMemoryReactiveClientRegistrationRepository(List<ClientRegistration> registrations) {
this.delegate = new InMemoryClientRegistrationRepository(registrations);
this.clientIdToClientRegistration = toUnmodifiableConcurrentMap(registrations);
}
@Override
public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
return Mono.justOrEmpty(this.delegate.findByRegistrationId(registrationId));
return Mono.justOrEmpty(this.clientIdToClientRegistration.get(registrationId));
}
/**
@@ -63,6 +75,20 @@ public final class InMemoryReactiveClientRegistrationRepository
*/
@Override
public Iterator<ClientRegistration> iterator() {
return delegate.iterator();
return this.clientIdToClientRegistration.values().iterator();
}
private static Map<String, ClientRegistration> toUnmodifiableConcurrentMap(List<ClientRegistration> registrations) {
Assert.notEmpty(registrations, "registrations cannot be null or empty");
ConcurrentHashMap<String, ClientRegistration> result = new ConcurrentHashMap<>();
for (ClientRegistration registration : registrations) {
Assert.notNull(registration, "no registration can be null");
if (result.containsKey(registration.getRegistrationId())) {
throw new IllegalStateException(String.format("Duplicate key %s",
registration.getRegistrationId()));
}
result.put(registration.getRegistrationId(), registration);
}
return Collections.unmodifiableMap(result);
}
}
@@ -133,7 +133,7 @@ class OAuth2AuthorizedClientResolver {
});
}
private Mono<? extends OAuth2AuthorizedClient> clientCredentials(
Mono<OAuth2AuthorizedClient> clientCredentials(
ClientRegistration clientRegistration, Authentication authentication, ServerWebExchange exchange) {
OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
return this.clientCredentialsTokenResponseClient.getTokenResponse(grantRequest)
@@ -85,8 +85,12 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
private final OAuth2AuthorizedClientResolver authorizedClientResolver;
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this(authorizedClientRepository, new OAuth2AuthorizedClientResolver(clientRegistrationRepository, authorizedClientRepository));
}
ServerOAuth2AuthorizedClientExchangeFilterFunction(ServerOAuth2AuthorizedClientRepository authorizedClientRepository, OAuth2AuthorizedClientResolver authorizedClientResolver) {
this.authorizedClientRepository = authorizedClientRepository;
this.authorizedClientResolver = new OAuth2AuthorizedClientResolver(clientRegistrationRepository, authorizedClientRepository);
this.authorizedClientResolver = authorizedClientResolver;
}
/**
@@ -246,13 +250,30 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
}
private Mono<OAuth2AuthorizedClient> refreshIfNecessary(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) {
if (shouldRefresh(authorizedClient)) {
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
if (isClientCredentialsGrantType(clientRegistration) && hasTokenExpired(authorizedClient)) {
return createRequest(request)
.flatMap(r -> authorizeWithClientCredentials(clientRegistration, r));
} else if (shouldRefresh(authorizedClient)) {
return createRequest(request)
.flatMap(r -> refreshAuthorizedClient(next, authorizedClient, r));
}
return Mono.just(authorizedClient);
}
private boolean isClientCredentialsGrantType(ClientRegistration clientRegistration) {
return AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType());
}
private Mono<OAuth2AuthorizedClient> authorizeWithClientCredentials(ClientRegistration clientRegistration, OAuth2AuthorizedClientResolver.Request request) {
Authentication authentication = request.getAuthentication();
ServerWebExchange exchange = request.getExchange();
return this.authorizedClientResolver.clientCredentials(clientRegistration, authentication, exchange).
flatMap(result -> this.authorizedClientRepository.saveAuthorizedClient(result, authentication, exchange)
.thenReturn(result));
}
private Mono<OAuth2AuthorizedClient> refreshAuthorizedClient(ExchangeFunction next,
OAuth2AuthorizedClient authorizedClient, OAuth2AuthorizedClientResolver.Request r) {
ServerWebExchange exchange = r.getExchange();
@@ -285,6 +306,10 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
if (refreshToken == null) {
return false;
}
return hasTokenExpired(authorizedClient);
}
private boolean hasTokenExpired(OAuth2AuthorizedClient authorizedClient) {
Instant now = this.clock.instant();
Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt();
if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) {
@@ -22,6 +22,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@@ -103,6 +104,7 @@ import static org.springframework.security.oauth2.core.web.reactive.function.OAu
* </ul>
*
* @author Rob Winch
* @author Roman Matiushchenko
* @since 5.1
*/
public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
@@ -146,7 +148,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
@Override
public void afterPropertiesSet() throws Exception {
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY, Operators.lift((s, sub) -> createRequestContextSubscriber(sub)));
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY, Operators.liftPublisher((s, sub) -> createRequestContextSubscriberIfNecessary(sub)));
}
@Override
@@ -319,14 +321,22 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
}
private void populateRequestAttributes(Map<String, Object> attrs, Context ctx) {
if (ctx.hasKey(HTTP_SERVLET_REQUEST_ATTR_NAME)) {
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, ctx.get(HTTP_SERVLET_REQUEST_ATTR_NAME));
}
if (ctx.hasKey(HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, ctx.get(HTTP_SERVLET_RESPONSE_ATTR_NAME));
}
if (ctx.hasKey(AUTHENTICATION_ATTR_NAME)) {
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, ctx.get(AUTHENTICATION_ATTR_NAME));
RequestContextDataHolder holder = RequestContextSubscriber.getRequestContext(ctx);
if (holder != null) {
HttpServletRequest request = holder.getRequest();
if (request != null) {
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request);
}
HttpServletResponse response = holder.getResponse();
if (response != null) {
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response);
}
Authentication authentication = holder.getAuthentication();
if (authentication != null) {
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, authentication);
}
}
populateDefaultOAuth2AuthorizedClient(attrs);
}
@@ -402,6 +412,10 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
throw new ClientAuthorizationRequiredException(clientRegistrationId);
}
private boolean isClientCredentialsGrantType(ClientRegistration clientRegistration) {
return AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType());
}
private OAuth2AuthorizedClient getAuthorizedClient(ClientRegistration clientRegistration,
Map<String, Object> attrs) {
@@ -429,7 +443,11 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
}
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) {
if (shouldRefresh(authorizedClient)) {
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
if (isClientCredentialsGrantType(clientRegistration) && hasTokenExpired(authorizedClient)) {
//Client credentials grant do not have refresh tokens but can expire so we need to get another one
return Mono.fromSupplier(() -> getAuthorizedClient(clientRegistration, request.attributes()));
} else if (shouldRefresh(authorizedClient)) {
return refreshAuthorizedClient(request, next, authorizedClient);
}
return Mono.just(authorizedClient);
@@ -474,6 +492,10 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
if (refreshToken == null) {
return false;
}
return hasTokenExpired(authorizedClient);
}
private boolean hasTokenExpired(OAuth2AuthorizedClient authorizedClient) {
Instant now = this.clock.instant();
Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt();
if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) {
@@ -488,7 +510,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
.build();
}
private <T> CoreSubscriber<T> createRequestContextSubscriber(CoreSubscriber<T> delegate) {
<T> CoreSubscriber<T> createRequestContextSubscriberIfNecessary(CoreSubscriber<T> delegate) {
HttpServletRequest request = null;
HttpServletResponse response = null;
ServletRequestAttributes requestAttributes =
@@ -498,6 +520,10 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
response = requestAttributes.getResponse();
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null && request == null && response == null) {
//do not need to create RequestContextSubscriber with empty data
return delegate;
}
return new RequestContextSubscriber<>(delegate, request, response, authentication);
}
@@ -575,34 +601,37 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
}
}
private static class RequestContextSubscriber<T> implements CoreSubscriber<T> {
private static final String CONTEXT_DEFAULTED_ATTR_NAME = RequestContextSubscriber.class.getName().concat(".CONTEXT_DEFAULTED_ATTR_NAME");
static class RequestContextSubscriber<T> implements CoreSubscriber<T> {
static final String REQUEST_CONTEXT_DATA_HOLDER =
RequestContextSubscriber.class.getName().concat(".REQUEST_CONTEXT_DATA_HOLDER");
private final CoreSubscriber<T> delegate;
private final HttpServletRequest request;
private final HttpServletResponse response;
private final Authentication authentication;
private final Context context;
private RequestContextSubscriber(CoreSubscriber<T> delegate,
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
RequestContextSubscriber(CoreSubscriber<T> delegate,
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
this.delegate = delegate;
this.request = request;
this.response = response;
this.authentication = authentication;
Context parentContext = this.delegate.currentContext();
Context context;
if (parentContext.hasKey(REQUEST_CONTEXT_DATA_HOLDER)) {
context = parentContext;
} else {
context = parentContext.put(REQUEST_CONTEXT_DATA_HOLDER, new RequestContextDataHolder(request, response, authentication));
}
this.context = context;
}
@Nullable
private static RequestContextDataHolder getRequestContext(Context ctx) {
return ctx.getOrDefault(REQUEST_CONTEXT_DATA_HOLDER, null);
}
@Override
public Context currentContext() {
Context context = this.delegate.currentContext();
if (context.hasKey(CONTEXT_DEFAULTED_ATTR_NAME)) {
return context;
}
return Context.of(
CONTEXT_DEFAULTED_ATTR_NAME, Boolean.TRUE,
HTTP_SERVLET_REQUEST_ATTR_NAME, this.request,
HTTP_SERVLET_RESPONSE_ATTR_NAME, this.response,
AUTHENTICATION_ATTR_NAME, this.authentication);
return this.context;
}
@Override
@@ -625,4 +654,33 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
this.delegate.onComplete();
}
}
static class RequestContextDataHolder {
private final HttpServletRequest request;
private final HttpServletResponse response;
private final Authentication authentication;
RequestContextDataHolder(@Nullable HttpServletRequest request,
@Nullable HttpServletResponse response,
@Nullable Authentication authentication) {
this.request = request;
this.response = response;
this.authentication = authentication;
}
@Nullable
private HttpServletRequest getRequest() {
return this.request;
}
@Nullable
private HttpServletResponse getResponse() {
return this.response;
}
@Nullable
private Authentication getAuthentication() {
return this.authentication;
}
}
}
@@ -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.
@@ -35,12 +35,13 @@ import org.springframework.security.web.server.authentication.RedirectServerAuth
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
/**
@@ -71,6 +72,7 @@ import reactor.core.publisher.Mono;
* </ul>
*
* @author Rob Winch
* @author Joe Grandja
* @since 5.1
* @see OAuth2AuthorizationCodeAuthenticationToken
* @see org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager
@@ -89,6 +91,9 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
new WebSessionOAuth2ServerAuthorizationRequestRepository();
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ServerAuthenticationConverter authenticationConverter;
@@ -109,7 +114,7 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
this.authenticationManager = authenticationManager;
this.authorizedClientRepository = authorizedClientRepository;
this.requiresAuthenticationMatcher = new PathPatternParserServerWebExchangeMatcher("/{action}/oauth2/code/{registrationId}");
this.requiresAuthenticationMatcher = this::matchesAuthorizationResponse;
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
@@ -124,7 +129,7 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
this.authenticationManager = authenticationManager;
this.authorizedClientRepository = authorizedClientRepository;
this.requiresAuthenticationMatcher = new PathPatternParserServerWebExchangeMatcher("/{action}/oauth2/code/{registrationId}");
this.requiresAuthenticationMatcher = this::matchesAuthorizationResponse;
this.authenticationConverter = authenticationConverter;
this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
@@ -164,4 +169,22 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
.flatMap(principal -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, webFilterExchange.getExchange()))
);
}
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
.flatMap(authorizationRequest -> {
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.query(null)
.build()
.toUriString();
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(queryParams)) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
})
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
}
}
@@ -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.
@@ -85,6 +85,9 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
OAuth2AuthorizationRequest removedValue = stateToAuthzRequest.remove(state);
if (stateToAuthzRequest.isEmpty()) {
sessionAttrs.remove(this.sessionAttributeName);
} else if (removedValue != null) {
// gh-7327 Overwrite the existing Map to ensure the state is saved for distributed sessions
sessionAttrs.put(this.sessionAttributeName, stateToAuthzRequest);
}
if (removedValue == null) {
sink.complete();
@@ -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.
@@ -108,18 +108,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthorizationException() {
when(this.authorizationRequest.getRedirectUri()).thenReturn("https://example.com");
when(this.authorizationResponse.getRedirectUri()).thenReturn("https://example2.com");
assertThatThrownBy(() -> {
this.authenticationProvider.authenticate(
new OAuth2AuthorizationCodeAuthenticationToken(
this.clientRegistration, this.authorizationExchange));
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
}
@Test
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
@@ -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.
@@ -80,13 +80,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.
@@ -154,18 +154,6 @@ public class OAuth2LoginAuthenticationProviderTests {
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
when(this.authorizationRequest.getRedirectUri()).thenReturn("https://example.com");
when(this.authorizationResponse.getRedirectUri()).thenReturn("https://example2.com");
this.authenticationProvider.authenticate(
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
}
@Test
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
@@ -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.
@@ -169,18 +169,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
when(this.authorizationRequest.getRedirectUri()).thenReturn("https://example1.com");
when(this.authorizationResponse.getRedirectUri()).thenReturn("https://example2.com");
this.authenticationProvider.authenticate(
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
}
@Test
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
@@ -44,6 +44,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
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.client.web.reactive.function.client.OAuth2AuthorizedClientResolver.Request;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
@@ -69,6 +70,7 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@@ -88,6 +90,9 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
@Mock
private ReactiveClientRegistrationRepository clientRegistrationRepository;
@Mock
private OAuth2AuthorizedClientResolver oAuth2AuthorizedClientResolver;
@Mock
private ServerWebExchange serverWebExchange;
@@ -149,6 +154,88 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue());
}
@Test
public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() {
TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this");
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
String clientRegistrationId = registration.getClientId();
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository, this.oAuth2AuthorizedClientResolver);
OAuth2AccessToken newAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"new-token",
Instant.now(),
Instant.now().plus(Duration.ofDays(1)));
OAuth2AuthorizedClient newAuthorizedClient = new OAuth2AuthorizedClient(registration,
"principalName", newAccessToken, null);
Request r = new Request(clientRegistrationId, authentication, null);
when(this.oAuth2AuthorizedClientResolver.clientCredentials(any(), any(), any())).thenReturn(Mono.just(newAuthorizedClient));
when(this.oAuth2AuthorizedClientResolver.createDefaultedRequest(any(), any(), any())).thenReturn(Mono.just(r));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1));
OAuth2AccessToken accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(),
this.accessToken.getTokenValue(),
issuedAt,
accessTokenExpiresAt);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(registration,
"principalName", accessToken, null);
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(oauth2AuthorizedClient(authorizedClient))
.build();
this.function.filter(request, this.exchange)
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
.block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any());
verify(this.oAuth2AuthorizedClientResolver).clientCredentials(any(), any(), any());
verify(this.oAuth2AuthorizedClientResolver).createDefaultedRequest(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();
}
@Test
public void filterWhenClientCredentialsTokenNotExpiredThenUseCurrentToken() {
TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this");
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository, this.oAuth2AuthorizedClientResolver);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(registration,
"principalName", this.accessToken, null);
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(oauth2AuthorizedClient(authorizedClient))
.build();
this.function.filter(request, this.exchange)
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
.block();
verify(this.oAuth2AuthorizedClientResolver, never()).clientCredentials(any(), any(), any());
verify(this.oAuth2AuthorizedClientResolver, never()).createDefaultedRequest(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 token-0");
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
assertThat(getBody(request1)).isEmpty();
}
@Test
public void filterWhenRefreshRequiredThenRefresh() {
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
@@ -55,13 +55,17 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepo
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.net.URI;
import java.time.Duration;
@@ -74,9 +78,14 @@ import java.util.Optional;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*;
@@ -124,9 +133,10 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
}
@After
public void cleanup() {
public void cleanup() throws Exception {
SecurityContextHolder.clearContext();
RequestContextHolder.resetRequestAttributes();
this.function.destroy();
}
@Test
@@ -428,6 +438,80 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
assertThat(getBody(request1)).isEmpty();
}
@Test
public void filterWhenClientCredentialsTokenNotExpiredThenUseCurrentToken() {
this.registration = TestClientRegistrations.clientCredentials().build();
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository,
this.authorizedClientRepository);
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
"principalName", this.accessToken, null);
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(oauth2AuthorizedClient(authorizedClient))
.attributes(authentication(this.authentication))
.build();
this.function.filter(request, this.exchange).block();
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), eq(this.authentication), any(), any());
verify(clientCredentialsTokenResponseClient, never()).getTokenResponse(any());
List<ClientRequest> requests = this.exchange.getRequests();
assertThat(requests).hasSize(1);
ClientRequest request1 = requests.get(0);
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0");
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
assertThat(getBody(request1)).isEmpty();
}
@Test
public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() {
this.registration = TestClientRegistrations.clientCredentials().build();
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses
.accessTokenResponse().build();
when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(
accessTokenResponse);
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1));
this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(),
this.accessToken.getTokenValue(),
issuedAt,
accessTokenExpiresAt);
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository,
this.authorizedClientRepository);
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
"principalName", this.accessToken, null);
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(oauth2AuthorizedClient(authorizedClient))
.attributes(authentication(this.authentication))
.build();
this.function.filter(request, this.exchange).block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(this.authentication), any(), any());
verify(clientCredentialsTokenResponseClient).getTokenResponse(any());
List<ClientRequest> requests = this.exchange.getRequests();
assertThat(requests).hasSize(1);
ClientRequest request1 = requests.get(0);
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token");
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
assertThat(getBody(request1)).isEmpty();
}
@Test
public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() {
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
@@ -636,6 +720,90 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
assertThat(getBody(request)).isEmpty();
}
// gh-7228
@Test
public void afterPropertiesSetWhenHooksInitAndOutsideWebSecurityContextThenShouldNotThrowException() throws Exception {
this.function.afterPropertiesSet(); // Hooks.onLastOperator() initialized
assertThatCode(() -> Mono.subscriberContext().block())
.as("RequestContext Hook brakes application outside of web/security context")
.doesNotThrowAnyException();
}
@Test
public void createRequestContextSubscriberIfNecessaryWhenOutsideWebSecurityContextThenReturnOriginalSubscriber() throws Exception {
BaseSubscriber<Object> originalSubscriber = new BaseSubscriber<Object>() {};
CoreSubscriber<Object> resultSubscriber = this.function.createRequestContextSubscriberIfNecessary(originalSubscriber);
assertThat(resultSubscriber).isSameAs(originalSubscriber);
}
// gh-7228
@Test
public void createRequestContextSubscriberWhenRequestResponseProvidedThenCreateWithParentContext() throws Exception {
testRequestContextSubscriber(new MockHttpServletRequest(), new MockHttpServletResponse(), null);
}
// gh-7228
@Test
public void createRequestContextSubscriberWhenAuthenticationProvidedThenCreateWithParentContext() throws Exception {
testRequestContextSubscriber(null, null, this.authentication);
}
@Test
public void createRequestContextSubscriberWhenParentContextHasDataHolderThenShouldReuseParentContext() throws Exception {
RequestContextDataHolder testValue = new RequestContextDataHolder(null, null, null);
final Context parentContext = Context.of(RequestContextSubscriber.REQUEST_CONTEXT_DATA_HOLDER, testValue);
BaseSubscriber<Object> parent = new BaseSubscriber<Object>() {
@Override
public Context currentContext() {
return parentContext;
}
};
RequestContextSubscriber<Object> requestContextSubscriber =
new RequestContextSubscriber<>(parent, null, null, authentication);
Context resultContext = requestContextSubscriber.currentContext();
assertThat(resultContext)
.describedAs("parent context was replaced")
.isSameAs(parentContext);
}
private void testRequestContextSubscriber(MockHttpServletRequest servletRequest,
MockHttpServletResponse servletResponse,
Authentication authentication) {
String testKey = "test_key";
String testValue = "test_value";
BaseSubscriber<Object> parent = new BaseSubscriber<Object>() {
@Override
public Context currentContext() {
return Context.of(testKey, testValue);
}
};
RequestContextSubscriber<Object> requestContextSubscriber =
new RequestContextSubscriber<>(parent, servletRequest, servletResponse, authentication);
Context resultContext = requestContextSubscriber.currentContext();
assertThat(resultContext)
.describedAs("result context is null")
.isNotNull();
assertThat(resultContext.getOrEmpty(testKey))
.describedAs("context is replaced")
.hasValue(testValue);
Object dataHolder = resultContext.getOrDefault(RequestContextSubscriber.REQUEST_CONTEXT_DATA_HOLDER, null);
assertThat(dataHolder)
.describedAs("context is not populated with REQUEST_CONTEXT_DATA_HOLDER")
.isNotNull()
.hasFieldOrPropertyWithValue("request", servletRequest)
.hasFieldOrPropertyWithValue("response", servletResponse)
.hasFieldOrPropertyWithValue("authentication", authentication);
}
private static String getBody(ClientRequest request) {
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
@@ -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.
@@ -28,16 +28,22 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
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.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
/**
* @author Rob Winch
@@ -53,6 +59,9 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
@Mock
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
new WebSessionOAuth2ServerAuthorizationRequestRepository();
@Before
public void setup() {
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
@@ -101,25 +110,42 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
@Test
public void filterWhenMatchThenAuthorizedClientSaved() {
Mono<Authentication> authentication = Mono
.just(TestOAuth2AuthorizationCodeAuthenticationTokens.unauthenticated());
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
.redirectUri("/authorize/registration-id")
.build();
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
.redirectUri("/authorize/registration-id")
.build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
Mono<Authentication> authentication = Mono.just(
new OAuth2AuthorizationCodeAuthenticationToken(registration, authorizationExchange));
OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens
.authenticated();
ServerAuthenticationConverter converter = e -> authentication;
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
this.authenticationManager, converter, this.authorizedClientRepository);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest
.get("/authorize/oauth2/code/registration-id"));
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete());
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(
authenticated));
when(this.authenticationManager.authenticate(any())).thenReturn(
Mono.just(authenticated));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty());
ServerAuthenticationConverter converter = e -> authentication;
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
this.authenticationManager, converter, this.authorizedClientRepository);
MockServerHttpRequest request = MockServerHttpRequest
.get("/authorize/registration-id")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete());
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any());
}
}
@@ -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.
@@ -63,7 +63,7 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
.queryParam(OAuth2ParameterNames.STATE, "state"));
@Test
public void loadAuthorizatioNRequestWhenNullExchangeThenIllegalArgumentException() {
public void loadAuthorizationRequestWhenNullExchangeThenIllegalArgumentException() {
this.exchange = null;
assertThatThrownBy(() -> this.repository.loadAuthorizationRequest(this.exchange))
.isInstanceOf(IllegalArgumentException.class);
@@ -106,36 +106,6 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
.verifyComplete();
}
@Test
public void multipleSavedAuthorizationRequestAndRedisCookie() {
String oldState = "state0";
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState).build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
Map<String, Object> sessionAttrs = spy(new HashMap<>());
WebSession session = mock(WebSession.class);
when(session.getAttributes()).thenReturn(sessionAttrs);
WebSessionManager sessionManager = e -> Mono.just(session);
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(), sessionManager,
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(), sessionManager,
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
Mono<Void> saveAndSave = this.repository.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange));
StepVerifier.create(saveAndSave).verifyComplete();
verify(sessionAttrs, times(2)).put(any(), any());
}
@Test
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
String oldState = "state0";
@@ -269,6 +239,44 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
.verifyComplete();
}
// gh-7327
@Test
public void removeAuthorizationRequestWhenMultipleThenRemovedAndSessionAttributeUpdated() {
String oldState = "state0";
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState).build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
Map<String, Object> sessionAttrs = spy(new HashMap<>());
WebSession session = mock(WebSession.class);
when(session.getAttributes()).thenReturn(sessionAttrs);
WebSessionManager sessionManager = e -> Mono.just(session);
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(), sessionManager,
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(), sessionManager,
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.removeAuthorizationRequest(this.exchange));
StepVerifier.create(saveAndSaveAndRemove)
.expectNext(this.authorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.verifyComplete();
verify(sessionAttrs, times(3)).put(any(), any());
}
private void assertSessionStartedIs(boolean expected) {
Mono<Boolean> isStarted = this.exchange.getSession().map(WebSession::isStarted);
StepVerifier.create(isStarted)
@@ -20,6 +20,7 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -210,12 +211,13 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
}
private static class RestOperationsResourceRetriever implements ResourceRetriever {
private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
private RestOperations restOperations = new RestTemplate();
@Override
public Resource retrieveResource(URL url) throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
ResponseEntity<String> response;
try {
@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.jwt;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.nimbusds.jose.JWSAlgorithm;
@@ -31,16 +32,21 @@ import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
@@ -76,6 +82,8 @@ public class NimbusJwtDecoderJwkSupportTests {
private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A";
private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
private NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM);
@Test
@@ -256,4 +264,19 @@ public class NimbusJwtDecoderJwkSupportTests {
assertThatCode(() -> jwtDecoder.setClaimSetConverter(null))
.isInstanceOf(IllegalArgumentException.class);
}
// gh-7290
@Test
public void decodeWhenJwkSetRequestedThenAcceptHeaderJsonAndJwkSetJson() {
RestOperations restOperations = mock(RestOperations.class);
when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.thenReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL);
jwtDecoder.setRestOperations(restOperations);
jwtDecoder.decode(SIGNED_JWT);
ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class);
verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class));
List<MediaType> acceptHeader = requestEntityCaptor.getValue().getHeaders().getAccept();
assertThat(acceptHeader).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON);
}
}
@@ -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.
@@ -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) throws Exception {
assertThat(page.getTitleText()).isEqualTo("Please sign in");
+1
View File
@@ -12,6 +12,7 @@ dependencies {
provided 'javax.servlet:javax.servlet-api'
testCompile project(path : ':spring-security-config', configuration : 'tests')
testCompile 'com.fasterxml.jackson.core:jackson-databind'
testCompile 'io.projectreactor:reactor-test'
testCompile 'javax.xml.bind:jaxb-api'
@@ -16,6 +16,11 @@
package org.springframework.security.test.web.servlet.setup;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.security.config.BeanIds;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
@@ -23,6 +28,8 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter;
import org.springframework.web.context.WebApplicationContext;
import java.io.IOException;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext;
/**
@@ -34,12 +41,13 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
* @since 4.0
*/
final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter {
private Filter springSecurityFilterChain;
private final DelegateFilter delegateFilter;
/**
* Creates a new instance
*/
SecurityMockMvcConfigurer() {
this.delegateFilter = new DelegateFilter();
}
/**
@@ -47,30 +55,101 @@ final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter {
* @param springSecurityFilterChain the {@link javax.servlet.Filter} to use
*/
SecurityMockMvcConfigurer(Filter springSecurityFilterChain) {
this.springSecurityFilterChain = springSecurityFilterChain;
this.delegateFilter = new DelegateFilter(springSecurityFilterChain);
}
@Override
public void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> builder) {
builder.addFilters(this.delegateFilter);
}
@Override
public RequestPostProcessor beforeMockMvcCreated(
ConfigurableMockMvcBuilder<?> builder, WebApplicationContext context) {
String securityBeanId = BeanIds.SPRING_SECURITY_FILTER_CHAIN;
if (this.springSecurityFilterChain == null
if (getSpringSecurityFilterChain() == null
&& context.containsBean(securityBeanId)) {
this.springSecurityFilterChain = context.getBean(securityBeanId,
Filter.class);
setSpringSecurityFitlerChain(context.getBean(securityBeanId,
Filter.class));
}
if (this.springSecurityFilterChain == null) {
if (getSpringSecurityFilterChain() == null) {
throw new IllegalStateException(
"springSecurityFilterChain cannot be null. Ensure a Bean with the name "
+ securityBeanId
+ " implementing Filter is present or inject the Filter to be used.");
}
builder.addFilters(this.springSecurityFilterChain);
// This is used by other test support to obtain the FilterChainProxy
context.getServletContext().setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN,
this.springSecurityFilterChain);
getSpringSecurityFilterChain());
return testSecurityContext();
}
private void setSpringSecurityFitlerChain(Filter filter) {
this.delegateFilter.setDelegate(filter);
}
private Filter getSpringSecurityFilterChain() {
return this.delegateFilter.delegate;
}
/**
* Allows adding in {@link #afterConfigurerAdded(ConfigurableMockMvcBuilder)} to preserve Filter order and then
* lazily set the delegate in {@link #beforeMockMvcCreated(ConfigurableMockMvcBuilder, WebApplicationContext)}.
*
* {@link org.springframework.web.filter.DelegatingFilterProxy} is not used because it is not easy to lazily set
* the delegate or get the delegate which is necessary for the test infrastructure.
*/
static class DelegateFilter implements Filter {
private Filter delegate;
DelegateFilter() {
}
DelegateFilter(Filter delegate) {
this.delegate = delegate;
}
void setDelegate(Filter delegate) {
this.delegate = delegate;
}
Filter getDelegate() {
return this.delegate;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.delegate.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
this.delegate.doFilter(request, response, chain);
}
@Override
public void destroy() {
this.delegate.destroy();
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
@Override
public boolean equals(Object obj) {
return this.delegate.equals(obj);
}
@Override
public String toString() {
return this.delegate.toString();
}
}
}
@@ -44,7 +44,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
@RunWith(PowerMockRunner.class)
@PrepareOnlyThisForTest(WebTestUtils.class)
@PowerMockIgnore("javax.security.auth.*")
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
public class SecurityMockMvcRequestPostProcessorsAuthenticationTests {
@Captor
private ArgumentCaptor<SecurityContext> contextCaptor;
@@ -32,6 +32,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -42,6 +43,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
@RunWith(PowerMockRunner.class)
@PrepareOnlyThisForTest(WebTestUtils.class)
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
public class SecurityMockMvcRequestPostProcessorsSecurityContextTests {
@Captor
private ArgumentCaptor<SecurityContext> contextCaptor;
@@ -29,6 +29,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -39,6 +40,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
@RunWith(PowerMockRunner.class)
@PrepareOnlyThisForTest(WebTestUtils.class)
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
public class SecurityMockMvcRequestPostProcessorsTestSecurityContextTests {
@Mock
private SecurityContext context;
@@ -81,4 +83,4 @@ public class SecurityMockMvcRequestPostProcessorsTestSecurityContextTests {
spy(WebTestUtils.class);
when(WebTestUtils.getSecurityContextRepository(request)).thenReturn(repository);
}
}
}
@@ -32,6 +32,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -44,6 +45,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
@RunWith(PowerMockRunner.class)
@PrepareOnlyThisForTest(WebTestUtils.class)
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
public class SecurityMockMvcRequestPostProcessorsUserDetailsTests {
@Captor
private ArgumentCaptor<SecurityContext> contextCaptor;
@@ -35,6 +35,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -47,6 +48,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
@RunWith(PowerMockRunner.class)
@PrepareOnlyThisForTest(WebTestUtils.class)
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
public class SecurityMockMvcRequestPostProcessorsUserTests {
@Captor
private ArgumentCaptor<SecurityContext> contextCaptor;
@@ -15,21 +15,24 @@
*/
package org.springframework.security.test.web.servlet.setup;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
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.config.BeanIds;
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
import org.springframework.web.context.WebApplicationContext;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -56,9 +59,10 @@ public class SecurityMockMvcConfigurerTests {
returnFilterBean();
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer(this.filter);
configurer.afterConfigurerAdded(this.builder);
configurer.beforeMockMvcCreated(this.builder, this.context);
verify(this.builder).addFilters(this.filter);
assertFilterAdded(this.filter);
verify(this.servletContext).setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN,
this.filter);
}
@@ -68,27 +72,37 @@ public class SecurityMockMvcConfigurerTests {
returnFilterBean();
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer();
configurer.afterConfigurerAdded(this.builder);
configurer.beforeMockMvcCreated(this.builder, this.context);
verify(this.builder).addFilters(this.beanFilter);
assertFilterAdded(this.beanFilter);
}
@Test
public void beforeMockMvcCreatedNoBean() throws Exception {
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer(this.filter);
configurer.afterConfigurerAdded(this.builder);
configurer.beforeMockMvcCreated(this.builder, this.context);
verify(this.builder).addFilters(this.filter);
assertFilterAdded(this.filter);
}
@Test(expected = IllegalStateException.class)
public void beforeMockMvcCreatedNoFilter() throws Exception {
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer();
configurer.afterConfigurerAdded(this.builder);
configurer.beforeMockMvcCreated(this.builder, this.context);
}
private void assertFilterAdded(Filter filter) throws IOException, ServletException {
ArgumentCaptor<SecurityMockMvcConfigurer.DelegateFilter> filterArg = ArgumentCaptor.forClass(
SecurityMockMvcConfigurer.DelegateFilter.class);
verify(this.builder).addFilters(filterArg.capture());
assertThat(filterArg.getValue().getDelegate()).isEqualTo(filter);
}
private void returnFilterBean() {
when(this.context.containsBean(anyString())).thenReturn(true);
when(this.context.getBean(anyString(), eq(Filter.class)))
@@ -0,0 +1,87 @@
/*
* 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.test.web.servlet.setup;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.users.AuthenticationTestConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import javax.servlet.Filter;
import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Rob Winch
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
public class SecurityMockMvcConfigurersTests {
@Autowired
WebApplicationContext wac;
Filter noOpFilter = mock(Filter.class);
/**
* Since noOpFilter is first does not continue the chain, security will not be invoked and the status should be OK
*
* @throws Exception
*/
@Test
public void applySpringSecurityWhenAddFilterFirstThenFilterFirst() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilters(this.noOpFilter)
.apply(springSecurity())
.build();
mockMvc.perform(get("/"))
.andExpect(status().isOk());
}
/**
* Since noOpFilter is second security will be invoked and the status will be not OK. We know this because if noOpFilter
* were first security would not be invoked sincet noOpFilter does not continue the FilterChain
* @throws Exception
*/
@Test
public void applySpringSecurityWhenAddFilterSecondThenSecurityFirst() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.apply(springSecurity())
.addFilters(this.noOpFilter)
.build();
mockMvc.perform(get("/"))
.andExpect(status().is4xxClientError());
}
@Configuration
@EnableWebMvc
@EnableWebSecurity
@Import(AuthenticationTestConfiguration.class)
static class Config {}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -15,6 +15,7 @@
*/
package org.springframework.security.web.authentication.session;
import java.util.Comparator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -45,8 +46,9 @@ import org.springframework.util.Assert;
* </p>
* <p>
* If a user has reached the maximum number of permitted sessions, the behaviour depends
* on the <tt>exceptionIfMaxExceeded</tt> property. The default behaviour is to expired
* the least recently used session, which will be invalidated by the
* on the <tt>exceptionIfMaxExceeded</tt> property. The default behaviour is to expire
* any sessions that exceed the maximum number of permitted sessions, starting with the
* least recently used sessions. The expired sessions will be invalidated by the
* {@link ConcurrentSessionFilter} if accessed again. If <tt>exceptionIfMaxExceeded</tt>
* is set to <tt>true</tt>, however, the user will be prevented from starting a new
* authenticated session.
@@ -156,18 +158,13 @@ public class ConcurrentSessionControlAuthenticationStrategy implements
"Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used session, and mark it for invalidation
SessionInformation leastRecentlyUsed = null;
for (SessionInformation session : sessions) {
if ((leastRecentlyUsed == null)
|| session.getLastRequest()
.before(leastRecentlyUsed.getLastRequest())) {
leastRecentlyUsed = session;
}
// Determine least recently used sessions, and mark them for invalidation
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session: sessionsToBeExpired) {
session.expireNow();
}
leastRecentlyUsed.expireNow();
}
/**
@@ -19,12 +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.stream.Collectors;
import java.util.stream.Stream;
/**
* Delegates to a collection of {@link ServerAuthenticationSuccessHandler} implementations.
@@ -43,7 +42,8 @@ public class DelegatingServerAuthenticationSuccessHandler implements ServerAuthe
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange,
Authentication authentication) {
Stream<Mono<Void>> results = this.delegates.stream().map(delegate -> delegate.onAuthenticationSuccess(exchange, authentication));
return Mono.when(results.collect(Collectors.toList()));
return Flux.fromIterable(this.delegates)
.concatMap(delegate -> delegate.onAuthenticationSuccess(exchange, authentication))
.then();
}
}
@@ -20,9 +20,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
@@ -50,10 +49,8 @@ public class DelegatingServerLogoutHandler implements ServerLogoutHandler {
@Override
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
return Mono.when(this.delegates.stream()
.filter(Objects::nonNull)
.map(delegate -> delegate.logout(exchange, authentication))
.collect(Collectors.toList())
);
return Flux.fromIterable(this.delegates)
.concatMap(delegate -> delegate.logout(exchange, authentication))
.then();
}
}
@@ -15,14 +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.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Combines multiple {@link ServerHttpHeadersWriter} instances into a single instance.
@@ -43,8 +41,9 @@ public class CompositeServerHttpHeadersWriter implements ServerHttpHeadersWriter
@Override
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
Stream<Mono<Void>> results = writers.stream().map( writer -> writer.writeHttpHeaders(exchange));
return Mono.when(results.collect(Collectors.toList()));
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>");
@@ -69,6 +69,12 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
super.setContentLength(len);
}
@Override
public void setContentLengthLong(long len) {
setContentLength(len);
super.setContentLengthLong(len);
}
private void setContentLength(long len) {
this.contentLength = len;
checkContentLength(0);
@@ -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();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 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.
@@ -134,6 +134,25 @@ public class ConcurrentSessionControlAuthenticationStrategyTests {
assertThat(sessionInformation.isExpired()).isTrue();
}
@Test
public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpired() {
SessionInformation oldestSessionInfo = new SessionInformation(
authentication.getPrincipal(), "unique1", new Date(1374766134214L));
SessionInformation secondOldestSessionInfo = new SessionInformation(
authentication.getPrincipal(), "unique2", new Date(1374766134215L));
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(
Arrays.<SessionInformation> asList(oldestSessionInfo,
secondOldestSessionInfo,
sessionInformation));
strategy.setMaximumSessions(2);
strategy.onAuthentication(authentication, request, response);
assertThat(oldestSessionInfo.isExpired()).isTrue();
assertThat(secondOldestSessionInfo.isExpired()).isTrue();
assertThat(sessionInformation.isExpired()).isFalse();
}
@Test(expected = IllegalArgumentException.class)
public void setMessageSourceNull() {
strategy.setMessageSource(null);
@@ -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();
}
}
@@ -16,8 +16,6 @@
package org.springframework.security.web.server.csrf;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import org.junit.Test;
@@ -28,6 +26,8 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eric Deandrea
* @since 5.1
@@ -111,7 +111,7 @@ public class CookieServerCsrfTokenRepositoryTests {
@Test
public void saveTokenWhenCustomPropertiesThenCustomProperties() {
setExpectedDomain(".spring.io");
setExpectedDomain("spring.io");
setExpectedCookieName("csrfCookie");
setExpectedPath("/some/path");
setExpectedHeaderName("headerName");
@@ -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();
}
}
@@ -1101,6 +1101,17 @@ public class OnCommittedResponseWrapperTests {
assertThat(committed).isTrue();
}
// gh-7261
@Test
public void contentLengthLongOutputStreamWriteStringCommits() throws IOException {
String body = "something";
response.setContentLengthLong(body.length());
response.getOutputStream().print(body);
assertThat(committed).isTrue();
}
@Test
public void addHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
int expected = 1234;