1
0
mirror of synced 2026-07-14 15:15:11 +00:00

Compare commits

...

87 Commits

Author SHA1 Message Date
Eleftheria Stein f93e5afe16 Release 5.2.3.RELEASE 2020-04-01 12:35:09 -04:00
Eleftheria Stein 7b5bdac74d Update to OpenSAML 3.4.5
Fixes gh-8278
2020-04-01 10:01:53 -04:00
Eleftheria Stein 80a16e828f Update to hibernate-entitymanager 5.4.13.Final
Fixes gh-8277
2020-04-01 10:01:20 -04:00
Eleftheria Stein ed133d4b8d Update to hibernate-core 5.2.18.Final
Fixes gh-8276
2020-04-01 10:00:41 -04:00
Eleftheria Stein 433646b551 Update blockhound to 1.0.3.RELEASE
Fixes gh-8275
2020-04-01 09:59:58 -04:00
Eleftheria Stein dd6974812f Update to unboundid-ldapsdk 4.0.14
Fixes gh-8274
2020-04-01 09:59:33 -04:00
Eleftheria Stein 97f1e368ee Update to reactive-streams 1.0.3
Fixes gh-8279
2020-04-01 09:58:19 -04:00
Eleftheria Stein 7018e7ec51 Update to Jackson 2.10.3
Fixes gh-8258
2020-04-01 09:16:29 -04:00
Eleftheria Stein 0313757846 Update to okhttp 3.14.7
Fixes gh-8259
2020-04-01 09:15:38 -04:00
Eleftheria Stein a021cd3fd5 Update to mockwebserver 3.14.7
Fixes gh-8257
2020-04-01 09:15:12 -04:00
Eleftheria Stein a57f4bb025 Update RSocket to 1.0.0-RC6
Fixes gh-8256
2020-04-01 09:14:38 -04:00
Eleftheria Stein a02cd9d9e3 Upgrade to embedded Apache Tomcat 9.0.33
Fixes gh-8254
2020-04-01 09:13:59 -04:00
Eleftheria Stein eacede6171 Update to httpclient 4.5.12
Fixes gh-8253
2020-04-01 09:12:31 -04:00
Eleftheria Stein 08b635a85a Update to org.powermock 2.0.6
Fixes gh-8255
2020-04-01 09:12:07 -04:00
Eleftheria Stein e3c3601cf6 Update to Spring Boot 2.2.6.RELEASE
Fixes gh-8252
2020-04-01 09:11:30 -04:00
Eleftheria Stein 1ca3ea3a71 Update to GAE 1.9.79
Fixes gh-8251
2020-04-01 09:10:43 -04:00
Eleftheria Stein dbdf02e864 Update to Reactor Dysprosium-SR6
Fixes gh-8250
2020-04-01 09:09:42 -04:00
Eleftheria Stein 774dd7bb13 Update to Spring Data Moore-SR6
Fixes gh-8248
2020-04-01 09:09:06 -04:00
Eleftheria Stein 6a9585dc0d Update to Spring Framework 5.2.5
Fixes gh-8249
2020-03-31 18:06:26 -04:00
Eleftheria Stein 298d40b7a5 Update tests to use absolute paths
Fixes gh-8260
2020-03-31 18:05:27 -04:00
Rob Winch 615f9a3f05 Fix HttpServlet3RequestFactory Logout Handlers
Previously there was a problem with Servlet API logout integration
when Servlet API was configured before log out.

This ensures that logout handlers is a reference to the logout handlers
vs copying the logout handlers. This ensures that the ordering does not
matter.

Closes gh-4760
2020-03-30 20:51:25 -05:00
Rob Winch 32c3353921 SpringTestContext returns ConfigurableWebApplicationContext
Closes gh-8233
2020-03-30 20:51:25 -05:00
Ruby Hartono 401597c673 Improve OAuth2LoginAuthenticationProvider
1. update OAuth2LoginAuthenticationProvider to use
OAuth2AuthorizationCodeAuthenticationProvider
2. apply fix gh-5368 for OAuth2AuthorizationCodeAuthenticationProvider
to return additionalParameters value from accessTokenResponse

Fixes gh-5633
2020-03-30 21:09:17 -04:00
Josh Cummings 258627eaee SwitchUserFilter Defaults to POST
Fixes gh-4183
2020-03-27 14:40:38 -06:00
Eleftheria Stein 01f8eb3961 Update Encryptors documentation
Fixes gh-8208
2020-03-27 10:20:04 -04:00
Martin Nemec a9a9c2c0fd OAuth2 ClientRegistrations NPE fix when userinfo missing
Fixes gh-8187
2020-03-27 06:15:25 -04:00
Josh Cummings cb7786bf97 Malformed Bearer Token Returns 401 for WebFlux
Fixes gh-7668
2020-03-26 12:59:22 -06:00
Joe Grandja 4706b16a2b oauth2Login WebFlux does not auto-redirect for XHR request
Fixes gh-8118
2020-03-26 05:09:45 -04:00
Josh Cummings 98bd1a3f60 Polish Resource Server JWT Docs
Issue gh-5935
2020-03-25 16:33:31 -06:00
Joe Grandja f06aa724bf OAuth2ErrorHttpMessageConverter handles JSON object parameters
Fixes gh-8157
2020-03-24 14:57:24 -04:00
Joe Grandja 512ad9e7e4 Document AuthorizedClientServiceOAuth2AuthorizedClientManager
Fixes gh-8152
2020-03-19 20:14:15 -04:00
Eleftheria Stein 256aba7b37 Fix rsocket test
Request route that exists; add additional error message verification

Fixes gh-8154
2020-03-19 17:36:20 -04:00
Erik van Paassen 86e25ff2ab Fix typo in Javadoc of HttpSecurity#csrf()
`HttpSecurity#csrf()` obviously returns a `CsrfConfigurer`, while the Javadoc states that it returns the `ServletApiConfigurer`.
2020-03-17 13:36:34 -06:00
Zeeshan Adnan a49a325db2 Fix exception for empty basic auth header token
fixes spring-projectsgh-7976
2020-03-16 16:06:52 -04:00
Markus Engelbrecht 75f22285c6 Fix typo 'properites' in documentation
Fixes gh-8095
2020-03-11 11:01:06 -06:00
Josh Cummings 8fa16ce63e Update to Jetty 9.4.27
Fixes gh-7507
2020-03-09 10:03:18 -06:00
Clement Stoquart 32c02fbedb Remove empty relay state from redirect url 2020-03-04 12:47:03 -07:00
AmitB 96ff3a54a9 Fix typo in AntPathRequestMatcher contructor comment 2020-03-02 07:16:07 -06:00
Josh Cummings 9092115b8a Register Authentication Provider in Init Phase
Fixes gh-8031
2020-02-28 18:43:54 -07:00
Joe Grandja 3dbfef9ef1 OAuth2AccessTokenResponseHttpMessageConverter handles JSON object parameters
Fixes gh-6463
2020-02-24 15:58:25 -05:00
Joe Grandja 8acdb82e6a OAuth2AuthorizationCodeGrantWebFilter matches on query parameters
Fixes gh-7966
2020-02-10 15:28:06 -05:00
Rafael Renan Pacheco 5ce0ce3f38 Fix var typo and code readability 2020-02-10 12:06:30 -07:00
Joe Grandja 6141132cfa Fix test gh-7963 2020-02-10 05:53:00 -05:00
Joe Grandja cc7ea4acd3 OAuth2AuthorizationCodeGrantFilter matches on query parameters
Fixes gh-7963
2020-02-10 05:24:14 -05:00
Manuel Bleichenbacher 1e4736f9b3 Prevent double-escaping of authorize URL parameters
If the authorization URL in the OAuth2 provider configuration contained query parameters with escaped characters, these characters were escaped a second time. This commit fixes it.

It is relevant to support the OIDC claims parameter (see https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter).

Fixes gh-7871
2020-02-08 16:59:01 -05:00
Stephane Maldini 0012e24c46 Don't force downcasting of RequestAttributes to ServletRequestAttributes
Fixes gh-7953
2020-02-07 20:18:50 -05:00
Joe Grandja 2dc8147106 Add release-notes-sections.yml 2020-02-05 15:18:32 -05:00
Joe Grandja 1da8e9df13 Next Development Version 2020-02-05 11:03:09 -05:00
Joe Grandja 9a2b71d931 Release 5.2.2.RELEASE 2020-02-05 10:56:00 -05:00
Josh Cummings c4ccc96655 Polish Error Messages for OpaqueTokenIntrospectors 2020-02-05 07:16:37 -07:00
Joe Grandja 6c310213a8 Update to Spring Boot 2.2.4
Fixes gh-7909
2020-02-04 15:07:16 -05:00
Joe Grandja a5b6b9a398 Update to org.slf4j 1.7.30
Fixes gh-7908
2020-02-04 15:04:46 -05:00
Joe Grandja 9e6910273c Update to org.powermock 2.0.5
Fixes gh-7907
2020-02-04 14:56:28 -05:00
Joe Grandja ea809b01a6 Update to hibernate-validator 6.1.2.Final
Fixes gh-7906
2020-02-04 14:53:08 -05:00
Joe Grandja 8054239a12 Update to hibernate-entitymanager 5.4.10.Final
Fixes gh-7905
2020-02-04 14:51:05 -05:00
Joe Grandja 46486194c2 Update to org.aspectj 1.9.5
Fixes gh-7904
2020-02-04 14:44:05 -05:00
Joe Grandja 00b08bc725 Update to httpclient 4.5.11
Fixes gh-7903
2020-02-04 14:39:27 -05:00
Joe Grandja 6e0fbfcccd Update to commons-codec 1.14
Fixes gh-7899
2020-02-04 14:31:31 -05:00
Joe Grandja 87ea083520 Update to com.squareup.okhttp3 3.14.6
Fixes gh-7898
2020-02-04 14:24:11 -05:00
Joe Grandja 9db3f51f2a Update to Jackson 2.10.2
Fixes gh-7897
2020-02-04 14:06:11 -05:00
Joe Grandja 3cc4a945c6 Update to Reactor Dysprosium SR4
Fixes gh-7896
2020-02-04 14:03:06 -05:00
Joe Grandja dbc43fb47d Update to Spring Data Moore SR3
Fixes gh-7895
2020-02-04 14:02:57 -05:00
Joe Grandja ce6a0368bd Update to Spring Framework 5.2.3
Fixes gh-7894
2020-02-04 13:38:17 -05:00
Eleftheria Stein 9dd3dfe718 Fix requiresAuthenticationMatcher not being used
The custom server requiresAuthenticationMatcher was not always picked up

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

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

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

Fixes gh-7721

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

Set RequestCache on the Oauth2LoginSpec default authentication success handler

import static ReflectionTestUtils.getField

Feedback incorporated per

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

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

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

Handle empty mono returned from contextAttributesMapper.

Handle empty map returned from contextAttributesMapper.

Fix DefaultContextAttributesMapper so that it doesn't access ServerWebExchange.

Fix unit tests so that they pass.

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

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

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

This change also makes OAuth2Client and OAuth2Login authentication converters consistent.

Fixes gh-7675
2019-11-29 13:58:27 -05:00
Joe Grandja b905cb8aaa Polish OAuth2AuthorizedClientArgumentResolver 2019-11-28 10:31:29 -05:00
Joe Grandja 19c2209a12 ServerOAuth2AuthorizedClientExchangeFilterFunction works with UnAuthenticatedServerOAuth2AuthorizedClientRepository
Fixes gh-7544
2019-11-28 10:31:18 -05:00
Joe Grandja 18f48e4a16 DefaultReactiveOAuth2AuthorizedClientManager requires non-null serverWebExchange
Issue gh-7544
2019-11-28 10:31:06 -05:00
96 changed files with 2733 additions and 619 deletions
@@ -47,7 +47,7 @@ public class ServiceAuthenticationDetailsSource implements
// ===================================================================================================
/**
* Creates an implementation that uses the specified ServiceProperites and the default
* Creates an implementation that uses the specified ServiceProperties and the default
* CAS artifactParameterName.
*
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
@@ -1471,7 +1471,7 @@ public final class HttpSecurity extends
* }
* </pre>
*
* @return the {@link ServletApiConfigurer} for further customizations
* @return the {@link CsrfConfigurer} for further customizations
* @throws Exception
*/
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -23,6 +23,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import reactor.core.CoreSubscriber;
@@ -92,32 +93,21 @@ class SecurityReactorContextConfiguration {
}
private static boolean contextAttributesAvailable() {
HttpServletRequest servletRequest = null;
HttpServletResponse servletResponse = null;
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
servletRequest = requestAttributes.getRequest();
servletResponse = requestAttributes.getResponse();
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null || servletRequest != null || servletResponse != null) {
return true;
}
return false;
return SecurityContextHolder.getContext().getAuthentication() != null ||
RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes;
}
private static Map<Object, Object> getContextAttributes() {
HttpServletRequest servletRequest = null;
HttpServletResponse servletResponse = null;
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
servletRequest = requestAttributes.getRequest();
servletResponse = requestAttributes.getResponse();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
servletRequest = servletRequestAttributes.getRequest();
servletResponse = servletRequestAttributes.getResponse(); // possible null
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null && servletRequest == null && servletResponse == null) {
if (authentication == null && servletRequest == null) {
return Collections.emptyMap();
}
@@ -172,7 +172,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
/**
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
* set.
*
* @param successHandler the {@link AuthenticationSuccessHandler}.
@@ -162,6 +162,7 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
matchers.add(notXRequestedWith);
matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM));
return new AndRequestMatcher(matchers);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -222,9 +222,16 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
@Override
public void init(H http) {
validateConfiguration();
registerDefaultAccessDeniedHandler(http);
registerDefaultEntryPoint(http);
registerDefaultCsrfOverride(http);
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
if (authenticationProvider != null) {
http.authenticationProvider(authenticationProvider);
}
}
@Override
@@ -232,8 +239,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
BearerTokenResolver bearerTokenResolver = getBearerTokenResolver();
this.requestMatcher.setBearerTokenResolver(bearerTokenResolver);
validateConfiguration();
AuthenticationManagerResolver resolver = this.authenticationManagerResolver;
if (resolver == null) {
AuthenticationManager authenticationManager = getAuthenticationManager(http);
@@ -321,9 +326,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.decoder;
}
AuthenticationManager getAuthenticationManager(H http) {
AuthenticationProvider getAuthenticationProvider() {
if (this.authenticationManager != null) {
return this.authenticationManager;
return null;
}
JwtDecoder decoder = getJwtDecoder();
@@ -333,9 +338,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
JwtAuthenticationProvider provider =
new JwtAuthenticationProvider(decoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
AuthenticationProvider authenticationProvider = postProcess(provider);
return postProcess(provider);
}
http.authenticationProvider(authenticationProvider);
AuthenticationManager getAuthenticationManager(H http) {
if (this.authenticationManager != null) {
return this.authenticationManager;
}
return http.getSharedObject(AuthenticationManager.class);
}
@@ -391,16 +400,19 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.context.getBean(OpaqueTokenIntrospector.class);
}
AuthenticationProvider getAuthenticationProvider() {
if (this.authenticationManager != null) {
return null;
}
OpaqueTokenIntrospector introspector = getIntrospector();
return new OpaqueTokenAuthenticationProvider(introspector);
}
AuthenticationManager getAuthenticationManager(H http) {
if (this.authenticationManager != null) {
return this.authenticationManager;
}
OpaqueTokenIntrospector introspector = getIntrospector();
OpaqueTokenAuthenticationProvider provider =
new OpaqueTokenAuthenticationProvider(introspector);
http.authenticationProvider(provider);
return http.getSharedObject(AuthenticationManager.class);
}
}
@@ -439,6 +451,18 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
csrf.ignoringRequestMatchers(this.requestMatcher);
}
AuthenticationProvider getAuthenticationProvider() {
if (this.jwtConfigurer != null) {
return this.jwtConfigurer.getAuthenticationProvider();
}
if (this.opaqueTokenConfigurer != null) {
return this.opaqueTokenConfigurer.getAuthenticationProvider();
}
return null;
}
AuthenticationManager getAuthenticationManager(H http) {
if (this.jwtConfigurer != null) {
return this.jwtConfigurer.getAuthenticationManager(http);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -55,6 +55,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
@@ -76,9 +77,11 @@ import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserSer
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
@@ -103,6 +106,7 @@ 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.AnonymousAuthenticationWebFilter;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
@@ -984,9 +988,9 @@ public class ServerHttpSecurity {
private ServerWebExchangeMatcher authenticationMatcher;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ServerAuthenticationFailureHandler authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
private ServerAuthenticationFailureHandler authenticationFailureHandler;
/**
* Configures the {@link ReactiveAuthenticationManager} to use. The default is
@@ -1028,6 +1032,7 @@ public class ServerHttpSecurity {
/**
* The {@link ServerAuthenticationFailureHandler} used after authentication failure.
* Defaults to {@link RedirectServerAuthenticationFailureHandler} redirecting to "/login?error".
*
* @since 5.2
* @param authenticationFailureHandler the failure handler to use
@@ -1084,7 +1089,9 @@ public class ServerHttpSecurity {
private ServerAuthenticationConverter getAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
if (this.authenticationConverter == null) {
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
ServerOAuth2AuthorizationCodeAuthenticationTokenConverter authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
authenticationConverter.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
this.authenticationConverter = authenticationConverter;
}
return this.authenticationConverter;
}
@@ -1172,24 +1179,76 @@ public class ServerHttpSecurity {
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
MediaType.TEXT_HTML);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
Map<String, String> urlToText = http.oauth2Login.getLinks();
if (urlToText.size() == 1) {
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
} else {
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
}
setDefaultEntryPoints(http);
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
}
private void setDefaultEntryPoints(ServerHttpSecurity http) {
String defaultLoginPage = "/login";
Map<String, String> urlToText = http.oauth2Login.getLinks();
String providerLoginPage = null;
if (urlToText.size() == 1) {
providerLoginPage = urlToText.keySet().iterator().next();
}
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"),
MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
ServerWebExchangeMatcher xhrMatcher = exchange -> {
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
};
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(
notXhrMatcher, htmlMatcher);
if (providerLoginPage != null) {
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage);
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
RedirectServerAuthenticationEntryPoint entryPoint =
new RedirectServerAuthenticationEntryPoint(providerLoginPage);
entryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
}
RedirectServerAuthenticationEntryPoint defaultEntryPoint =
new RedirectServerAuthenticationEntryPoint(defaultLoginPage);
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandler == null) {
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
handler.setRequestCache(http.requestCache.requestCache);
this.authenticationSuccessHandler = handler;
}
return this.authenticationSuccessHandler;
}
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
if (this.authenticationFailureHandler == null) {
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
}
return this.authenticationFailureHandler;
}
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
}
@@ -1766,6 +1825,28 @@ public class ServerHttpSecurity {
}
}
private class BearerTokenAuthenticationWebFilter extends AuthenticationWebFilter {
private ServerAuthenticationFailureHandler authenticationFailureHandler;
BearerTokenAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return super.filter(exchange, chain)
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
.onAuthenticationFailure(webFilterExchange, e));
}
@Override
public void setAuthenticationFailureHandler(ServerAuthenticationFailureHandler authenticationFailureHandler) {
super.setAuthenticationFailureHandler(authenticationFailureHandler);
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
/**
* Configures JWT Resource Server Support
*/
@@ -1839,7 +1920,7 @@ public class ServerHttpSecurity {
protected void configure(ServerHttpSecurity http) {
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
AuthenticationWebFilter oauth2 = new BearerTokenAuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
http
@@ -1945,7 +2026,7 @@ public class ServerHttpSecurity {
protected void configure(ServerHttpSecurity http) {
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
AuthenticationWebFilter oauth2 = new BearerTokenAuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
@@ -2765,7 +2846,9 @@ public class ServerHttpSecurity {
protected void configure(ServerHttpSecurity http) {
if (this.csrfTokenRepository != null) {
this.filter.setCsrfTokenRepository(this.csrfTokenRepository);
http.logout().addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
if (ServerHttpSecurity.this.logout != null) {
ServerHttpSecurity.this.logout.addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
}
}
http.addFilterAt(this.filter, SecurityWebFiltersOrder.CSRF);
}
@@ -3022,8 +3105,12 @@ public class ServerHttpSecurity {
public FormLoginSpec loginPage(String loginPage) {
this.defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(loginPage);
this.authenticationEntryPoint = this.defaultEntryPoint;
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
if (this.requiresAuthenticationMatcher == null) {
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
}
if (this.authenticationFailureHandler == null) {
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
}
return this;
}
@@ -103,7 +103,9 @@ public class RSocketMessageHandlerITests {
.data(data)
.retrieveMono(String.class)
.block()
).isInstanceOf(ApplicationErrorException.class);
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Access Denied");
assertThat(this.controller.payloads).isEmpty();
}
@@ -116,7 +118,9 @@ public class RSocketMessageHandlerITests {
.data(data)
.retrieveMono(String.class)
.block()
).isInstanceOf(ApplicationErrorException.class);
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Invalid Credentials");
assertThat(this.controller.payloads).isEmpty();
}
@@ -149,12 +153,13 @@ public class RSocketMessageHandlerITests {
@Test
public void retrieveFluxWhenDataFluxAndSecureThenDenied() throws Exception {
Flux<String> data = Flux.just("a", "b", "c");
assertThatCode(() -> this.requester.route("secure.secure.retrieve-flux")
assertThatCode(() -> this.requester.route("secure.retrieve-flux")
.data(data, String.class)
.retrieveFlux(String.class)
.collectList()
.block()).isInstanceOf(
ApplicationErrorException.class);
.block()
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Access Denied");
assertThat(this.controller.payloads).isEmpty();
}
@@ -179,8 +184,9 @@ public class RSocketMessageHandlerITests {
.data(data)
.retrieveFlux(String.class)
.collectList()
.block()).isInstanceOf(
ApplicationErrorException.class);
.block()
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Access Denied");
assertThat(this.controller.payloads).isEmpty();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -28,6 +28,7 @@ import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.client.ClientRequest;
@@ -36,6 +37,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Operators;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
@@ -139,6 +141,52 @@ public class SecurityReactorContextConfigurationTests {
assertThat(resultContext).isSameAs(parentContext);
}
@Test
public void createSubscriberIfNecessaryWhenNotServletRequestAttributesThenStillCreate() {
RequestContextHolder.setRequestAttributes(
new RequestAttributes() {
@Override
public Object getAttribute(String name, int scope) {
return null;
}
@Override
public void setAttribute(String name, Object value, int scope) {
}
@Override
public void removeAttribute(String name, int scope) {
}
@Override
public String[] getAttributeNames(int scope) {
return new String[0];
}
@Override
public void registerDestructionCallback(String name, Runnable callback, int scope) {
}
@Override
public Object resolveReference(String key) {
return null;
}
@Override
public String getSessionId() {
return null;
}
@Override
public Object getSessionMutex() {
return null;
}
});
CoreSubscriber<Object> subscriber = this.subscriberRegistrar.createSubscriberIfNecessary(Operators.emptySubscriber());
assertThat(subscriber).isInstanceOf(SecurityReactorContextConfiguration.SecurityReactorContextSubscriber.class);
}
@Test
public void createPublisherWhenLastOperatorAddedThenSecurityContextAttributesAvailable() {
// Trigger the importing of SecurityReactorContextConfiguration via OAuth2ImportSelector
@@ -458,4 +458,25 @@ public class LogoutConfigurerTests {
@EnableWebSecurity
static class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
}
@Test
public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
this.spring.register(LogoutDisabledConfig.class).autowire();
this.mvc.perform(post("/logout")
.with(csrf()))
.andExpect(status().isNotFound());
}
@EnableWebSecurity
static class LogoutDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.logout()
.disable();
// @formatter:on
}
}
}
@@ -183,6 +183,21 @@ public class RequestCacheConfigurerTests {
// This is desirable since XHR requests are typically not invoked directly from the browser and we don't want the browser to replay them
}
@Test
public void getWhenBookmarkedRequestIsTextEventStreamThenPostAuthenticationRedirectsToRoot() throws Exception {
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
MockHttpSession session = (MockHttpSession)
this.mvc.perform(get("/messages")
.header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM))
.andExpect(redirectedUrl("http://localhost/login"))
.andReturn().getRequest().getSession();
this.mvc.perform(formLogin(session))
.andExpect(redirectedUrl("/")); // ignores text/event-stream
// This is desirable since event-stream requests are typically not invoked directly from the browser and we don't want the browser to replay them
}
@Test
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
@@ -22,6 +22,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationTrustResolver;
@@ -44,10 +45,14 @@ import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -60,6 +65,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
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;
@@ -329,6 +335,39 @@ public class ServletApiConfigurerTests {
}
}
@Test
public void logoutServletApiWhenCsrfDisabled() throws Exception {
ConfigurableWebApplicationContext context = this.spring.register(CsrfDisabledConfig.class).getContext();
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity())
.build();
MvcResult mvcResult = mockMvc.perform(get("/"))
.andReturn();
assertThat(mvcResult.getRequest().getSession(false)).isNull();
}
@Configuration
@EnableWebSecurity
static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf().disable();
// @formatter:on
}
@RestController
static class LogoutController {
@GetMapping("/")
String logout(HttpServletRequest request) throws ServletException {
request.getSession().setAttribute("foo", "bar");
request.logout();
return "logout";
}
}
}
private <T extends Filter> T getFilter(Class<T> filterClass) {
return (T) getFilters().stream()
.filter(filterClass::isInstance)
@@ -360,6 +360,18 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
}
// gh-8031
@Test
public void getWhenAnonymousDisabledThenAllows() throws Exception {
this.spring.register(RestOperationsConfig.class, AnonymousDisabledConfig.class).autowire();
mockRestOperations(jwks("Default"));
String token = token("ValidNoScopes");
this.mvc.perform(get("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isNotFound());
}
@Test
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
throws Exception {
@@ -730,7 +742,8 @@ public class OAuth2ResourceServerConfigurerTests {
@Test
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire())
assertThatCode(() -> this.spring
.register(MultipleBearerTokenResolverBeansConfig.class, JwtDecoderConfig.class).autowire())
.isInstanceOf(BeanCreationException.class)
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
}
@@ -1127,7 +1140,7 @@ public class OAuth2ResourceServerConfigurerTests {
.with(bearerToken("token")))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
containsString("Provided token [token] isn't active")));
containsString("Provided token isn't active")));
}
@Test
@@ -1469,6 +1482,22 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class AnonymousDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.anonymous().disable()
.oauth2ResourceServer()
.jwt();
// @formatter:on
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
@@ -17,7 +17,6 @@
package org.springframework.security.config.test;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.util.InMemoryXmlWebApplicationContext;
@@ -113,8 +112,10 @@ public class SpringTestContext implements Closeable {
return this;
}
public ConfigurableApplicationContext getContext() {
public ConfigurableWebApplicationContext getContext() {
if (!this.context.isRunning()) {
this.context.setServletContext(new MockServletContext());
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
return this.context;
@@ -33,9 +33,11 @@ import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverB
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
@@ -213,6 +215,62 @@ public class FormLoginTests {
homePage.assertAt();
}
@Test
public void formLoginWhenCustomAuthenticationFailureHandlerThenUsed() {
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
.pathMatchers("/login", "/failure").permitAll()
.anyExchange().authenticated()
.and()
.formLogin()
.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/failure"))
.and()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class)
.assertAt();
loginPage.loginForm()
.username("invalid")
.password("invalid")
.submit(HomePage.class);
assertThat(driver.getCurrentUrl()).endsWith("/failure");
}
@Test
public void formLoginWhenCustomRequiresAuthenticationMatcherThenUsed() {
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
.pathMatchers("/login", "/sign-in").permitAll()
.anyExchange().authenticated()
.and()
.formLogin()
.requiresAuthenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/sign-in"))
.and()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
driver.get("http://localhost/sign-in");
assertThat(driver.getCurrentUrl()).endsWith("/login?error");
}
@Test
public void authenticationSuccess() {
SecurityWebFilterChain securityWebFilter = this.http
@@ -164,4 +164,40 @@ public class LogoutSpecTests {
.assertAt()
.assertLogout();
}
@Test
public void logoutWhenDisabledThenPostToLogoutDoesNothing() {
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.formLogin().and()
.logout().disable()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
.assertAt();
FormLoginTests.HomePage homePage = loginPage.loginForm()
.username("user")
.password("password")
.submit(FormLoginTests.HomePage.class);
homePage.assertAt();
FormLoginTests.DefaultLogoutPage.to(driver)
.assertAt()
.logout();
homePage
.assertAt();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
@@ -70,6 +71,7 @@ import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtValidationException;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -184,6 +186,22 @@ public class OAuth2LoginTests {
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
}
// gh-8118
@Test
public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() {
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire();
this.client.get()
.uri("/")
.header("X-Requested-With", "XMLHttpRequest")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
}
@EnableWebFlux
static class WebFluxConfig { }
@EnableWebFluxSecurity
static class OAuth2LoginWithSingleClientRegistrations {
@Bean
@@ -518,6 +536,85 @@ public class OAuth2LoginTests {
verify(securityContextRepository).save(any(), any());
}
// gh-5562
@Test
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
OAuth2LoginWithCustomBeansConfig.class).autowire();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(this.springSecurity)
.build();
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
ServerAuthenticationConverter converter = config.authenticationConverter;
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
OAuth2Error oauth2Error = new OAuth2Error("invalid_request", "Invalid request", null);
when(tokenResponseClient.getTokenResponse(any())).thenThrow(new OAuth2AuthenticationException(oauth2Error));
webTestClient.get()
.uri("/login/oauth2/code/google")
.exchange()
.expectStatus()
.is3xxRedirection()
.expectHeader()
.valueEquals("Location", "/login?error");
}
// gh-6484
@Test
public void oauth2LoginWhenIdTokenValidationFailsThenDefaultRedirectToLogin() {
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
OAuth2LoginWithCustomBeansConfig.class).autowire();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(this.springSecurity)
.build();
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
ServerAuthenticationConverter converter = config.authenticationConverter;
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes())
.additionalParameters(additionalParameters)
.build();
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
when(tokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = config.jwtDecoderFactory;
OAuth2Error oauth2Error = new OAuth2Error("invalid_id_token", "Invalid ID Token", null);
when(jwtDecoderFactory.createDecoder(any())).thenReturn(token ->
Mono.error(new JwtValidationException("ID Token validation failed", Collections.singleton(oauth2Error))));
webTestClient.get()
.uri("/login/oauth2/code/google")
.exchange()
.expectStatus()
.is3xxRedirection()
.expectHeader()
.valueEquals("Location", "/login?error");
}
@Configuration
static class OAuth2LoginWithCustomBeansConfig {
@@ -172,6 +172,17 @@ public class OAuth2ResourceServerSpecTests {
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\""));
}
@Test
public void getWhenEmptyBearerTokenThenReturnsInvalidToken() {
this.spring.register(PublicKeyConfig.class).autowire();
this.client.get()
.headers(headers -> headers.add("Authorization", "Bearer "))
.exchange()
.expectStatus().isUnauthorized()
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\""));
}
@Test
public void getWhenValidTokenAndPublicKeyInLambdaThenReturnsOk() {
this.spring.register(PublicKeyInLambdaConfig.class, RootController.class).autowire();
@@ -20,10 +20,12 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.test.util.ReflectionTestUtils.getField;
import java.util.Arrays;
import java.util.List;
@@ -35,12 +37,20 @@ import org.apache.http.HttpHeaders;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
import reactor.core.publisher.Mono;
import reactor.test.publisher.TestPublisher;
@@ -60,7 +70,6 @@ import org.springframework.security.web.server.context.WebSessionServerSecurityC
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -196,7 +205,7 @@ public class ServerHttpSecurityTests {
.isNotPresent();
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
assertThat(logoutHandler)
.get()
@@ -209,17 +218,17 @@ public class ServerHttpSecurityTests {
assertThat(getWebFilter(securityWebFilterChain, CsrfWebFilter.class))
.get()
.extracting(csrfWebFilter -> ReflectionTestUtils.getField(csrfWebFilter, "csrfTokenRepository"))
.extracting(csrfWebFilter -> getField(csrfWebFilter, "csrfTokenRepository"))
.isEqualTo(this.csrfTokenRepository);
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
assertThat(logoutHandler)
.get()
.isExactlyInstanceOf(DelegatingServerLogoutHandler.class)
.extracting(delegatingLogoutHandler ->
((List<ServerLogoutHandler>) ReflectionTestUtils.getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
((List<ServerLogoutHandler>) getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
.map(ServerLogoutHandler::getClass)
.collect(Collectors.toList()))
.isEqualTo(Arrays.asList(SecurityContextServerLogoutHandler.class, CsrfServerLogoutHandler.class));
@@ -475,9 +484,58 @@ public class ServerHttpSecurityTests {
verify(customServerCsrfTokenRepository).loadToken(any());
}
@Test
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
SecurityWebFilterChain securityFilterChain = this.http
.oauth2Login()
.clientRegistrationRepository(clientRegistrationRepository)
.and()
.authorizeExchange().anyExchange().authenticated()
.and()
.requestCache(c -> c.requestCache(requestCache))
.build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/test").exchange();
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
verify(requestCache).saveRequest(captor.capture());
assertThat(captor.getValue().getRequest().getURI().toString()).isEqualTo("/test");
OAuth2LoginAuthenticationWebFilter authenticationWebFilter =
getWebFilter(securityFilterChain, OAuth2LoginAuthenticationWebFilter.class).get();
Object handler = getField(authenticationWebFilter, "authenticationSuccessHandler");
assertThat(getField(handler, "requestCache")).isSameAs(requestCache);
}
@Test
public void shouldConfigureAuthorizationRequestRepositoryForOAuth2Login() {
ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = mock(ServerAuthorizationRequestRepository.class);
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request().build();
when(authorizationRequestRepository.removeAuthorizationRequest(any())).thenReturn(Mono.just(authorizationRequest));
SecurityWebFilterChain securityFilterChain = this.http
.oauth2Login()
.clientRegistrationRepository(clientRegistrationRepository)
.authorizationRequestRepository(authorizationRequestRepository)
.and()
.build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/login/oauth2/code/registration-id").exchange();
verify(authorizationRequestRepository).removeAuthorizationRequest(any());
}
private boolean isX509Filter(WebFilter filter) {
try {
Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");
Object converter = getField(filter, "authenticationConverter");
return converter.getClass().isAssignableFrom(ServerX509AuthenticationConverter.class);
} catch (IllegalArgumentException e) {
// field doesn't exist
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
* &lt;/property&gt;
* </pre>
*
* A configuration note: The JaasAuthenticationProvider uses the security properites
* A configuration note: The JaasAuthenticationProvider uses the security properties
* "login.config.url.X" to configure jaas. If you would like to customize the way Jaas
* gets configured, create a subclass of this and override the
* {@link #configureJaas(Resource)} method.
@@ -39,9 +39,6 @@ public class Encryptors {
* not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the
* key
*
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
* (instead of GCM)
*/
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt,
@@ -55,11 +52,19 @@ public class Encryptors {
* provided salt is expected to be hex-encoded; it should be random and at least 8
* bytes in length. Also applies a random 16 byte initialization vector to ensure each
* encrypted message will be unique. Requires Java 6.
* NOTE: This mode is not
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
* and does not provide any guarantees about the authenticity of the data.
* For a more secure alternative, users should prefer
* {@link #stronger(CharSequence, CharSequence)}.
*
* @param password the password used to generate the encryptor's secret key; should
* not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the
* key
*
* @see #stronger(CharSequence, CharSequence) which uses the significatly more secure
* GCM (instead of CBC)
*/
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt,
@@ -38,12 +38,12 @@ This configuration enables <<rsocket-authentication-basic,basic authentication>>
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
In a Spring Boot application this can be done using the following code.
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
[source,java]
----
@Bean
ServerRSocketFactoryCustomizer springSecurityRSocketSecurity(
ServerRSocketFactoryProcessor springSecurityRSocketSecurity(
SecuritySocketAcceptorInterceptor interceptor) {
return builder -> builder.addSocketAcceptorPlugin(interceptor);
}
@@ -2372,8 +2372,8 @@ A bean identifier, used for referring to the bean elsewhere in the context.
[[nsa-ldap-server-ldif]]
* **ldif**
Explicitly specifies an ldif file resource to load into an embedded LDAP server.
The ldiff is should be a Spring resource pattern (i.e. classpath:init.ldiff).
The default is classpath*:*.ldiff
The ldif should be a Spring resource pattern (i.e. classpath:init.ldif).
The default is classpath*:*.ldif
[[nsa-ldap-server-manager-dn]]
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
[[spring-security-crypto-encryption-bytes]]
=== BytesEncryptor
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor:
Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
[source,java]
----
Encryptors.standard("password", "salt");
Encryptors.stronger("password", "salt");
----
The "standard" encryption method is 256-bit AES using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
The "stronger" encryption method creates an encryptor using 256 bit AES encryption with
Galois Counter Mode (GCM).
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
This method requires Java 6.
The password used to generate the SecretKey should be kept in a secure place and not be shared.
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
@@ -38,6 +40,11 @@ Such a salt may be generated using a KeyGenerator:
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
----
Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
guarantees about the authenticity of the data.
For a more secure alternative, users should prefer `Encryptors.stronger`.
[[spring-security-crypto-encryption-text]]
=== TextEncryptor
Use the Encryptors.text factory method to construct a standard TextEncryptor:
@@ -343,6 +343,36 @@ private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesM
}
----
The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`.
When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead.
A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`.
Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account.
An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application.
The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type:
[source,java]
----
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
----
[[oauth2Client-auth-grant-support]]
=== Authorization Grant Support
@@ -77,12 +77,12 @@ So long as this scheme is indicated, Resource Server will attempt to process the
Given a well-formed JWT, Resource Server will:
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
3. Map each scope to an authority with the prefix `SCOPE_`.
[NOTE]
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate JWTs.
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
@@ -456,9 +456,11 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return jwtAuthenticationConveter;
return jwtAuthenticationConverter;
}
----
+4 -4
View File
@@ -1,5 +1,5 @@
aspectjVersion=1.9.3
gaeVersion=1.9.76
springBootVersion=2.2.0.RELEASE
version=5.2.2.BUILD-SNAPSHOT
aspectjVersion=1.9.5
gaeVersion=1.9.79
springBootVersion=2.2.6.RELEASE
version=5.2.3.RELEASE
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
+47 -47
View File
@@ -1,16 +1,16 @@
if (!project.hasProperty('reactorVersion')) {
ext.reactorVersion = 'Dysprosium-SR1'
ext.reactorVersion = 'Dysprosium-SR6'
}
if (!project.hasProperty('springVersion')) {
ext.springVersion = '5.2.1.RELEASE'
ext.springVersion = '5.2.5.RELEASE'
}
if (!project.hasProperty('springDataVersion')) {
ext.springDataVersion = 'Moore-SR1'
ext.springDataVersion = 'Moore-SR6'
}
ext.rsocketVersion = '1.0.0-RC5'
ext.rsocketVersion = '1.0.0-RC6'
dependencyManagement {
imports {
@@ -20,16 +20,16 @@ dependencyManagement {
}
dependencies {
dependency 'cglib:cglib-nodep:3.3.0'
dependency 'com.squareup.okhttp3:mockwebserver:3.14.2'
dependency 'com.squareup.okhttp3:mockwebserver:3.14.7'
dependency 'opensymphony:sitemesh:2.4.2'
dependency 'org.gebish:geb-spock:0.10.0'
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
dependency 'org.powermock:powermock-api-mockito2:2.0.4'
dependency 'org.powermock:powermock-api-support:2.0.4'
dependency 'org.powermock:powermock-core:2.0.4'
dependency 'org.powermock:powermock-module-junit4-common:2.0.4'
dependency 'org.powermock:powermock-module-junit4:2.0.4'
dependency 'org.powermock:powermock-reflect:2.0.4'
dependency 'org.powermock:powermock-api-mockito2:2.0.6'
dependency 'org.powermock:powermock-api-support:2.0.6'
dependency 'org.powermock:powermock-core:2.0.6'
dependency 'org.powermock:powermock-module-junit4-common:2.0.6'
dependency 'org.powermock:powermock-module-junit4:2.0.6'
dependency 'org.powermock:powermock-reflect:2.0.6'
dependency 'org.python:jython:2.5.0'
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
@@ -42,9 +42,9 @@ dependencyManagement {
dependency 'asm:asm:3.1'
dependency 'ch.qos.logback:logback-classic:1.2.3'
dependency 'ch.qos.logback:logback-core:1.2.3'
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.0'
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.0'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.3'
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.3'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.3'
dependency 'com.fasterxml:classmate:1.3.4'
dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1'
dependency 'com.google.appengine:appengine-api-1.0-sdk:$gaeVersion'
@@ -58,21 +58,21 @@ dependencyManagement {
dependency 'com.nimbusds:lang-tag:1.4.3'
dependency 'com.nimbusds:nimbus-jose-jwt:7.8.1'
dependency 'com.nimbusds:oauth2-oidc-sdk:6.14'
dependency 'com.squareup.okhttp3:okhttp:3.14.1'
dependency 'com.squareup.okhttp3:okhttp:3.14.7'
dependency 'com.squareup.okio:okio:1.13.0'
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2'
dependency 'com.unboundid:unboundid-ldapsdk:4.0.12'
dependency 'com.unboundid:unboundid-ldapsdk:4.0.14'
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
dependency 'commons-cli:commons-cli:1.4'
dependency 'commons-codec:commons-codec:1.13'
dependency 'commons-codec:commons-codec:1.14'
dependency 'commons-collections:commons-collections:3.2.2'
dependency 'commons-httpclient:commons-httpclient:3.1'
dependency 'commons-io:commons-io:2.6'
dependency 'commons-lang:commons-lang:2.6'
dependency 'commons-logging:commons-logging:1.2'
dependency 'dom4j:dom4j:1.6.1'
dependency 'io.projectreactor.tools:blockhound:1.0.1.RELEASE'
dependency 'io.projectreactor.tools:blockhound:1.0.3.RELEASE'
dependency "io.rsocket:rsocket-core:${rsocketVersion}"
dependency "io.rsocket:rsocket-transport-netty:${rsocketVersion}"
dependency 'javax.activation:activation:1.1.1'
@@ -138,19 +138,19 @@ dependencyManagement {
dependency 'org.apache.directory.shared:shared-cursor:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap:0.9.15'
dependency 'org.apache.httpcomponents:httpclient:4.5.10'
dependency 'org.apache.httpcomponents:httpclient:4.5.12'
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'
dependency 'org.apache.taglibs:taglibs-standard-impl:1.2.5'
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
dependency 'org.apache.taglibs:taglibs-standard-spec:1.2.5'
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.33'
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.33'
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.33'
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.33'
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.24'
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.33'
dependency "org.aspectj:aspectjrt:$aspectjVersion"
dependency "org.aspectj:aspectjtools:$aspectjVersion"
dependency "org.aspectj:aspectjweaver:$aspectjVersion"
@@ -162,17 +162,17 @@ dependencyManagement {
dependency 'org.codehaus.groovy:groovy-json:2.4.17'
dependency 'org.codehaus.groovy:groovy:2.4.17'
dependency 'org.eclipse.jdt:ecj:3.12.3'
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.19.v20190610'
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.19.v20190610'
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610'
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
dependency 'org.gebish:geb-ast:0.10.0'
dependency 'org.gebish:geb-core:0.10.0'
@@ -181,9 +181,9 @@ dependencyManagement {
dependency 'org.hamcrest:hamcrest-core:1.3'
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
dependency 'org.hibernate:hibernate-core:5.2.17.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.4.8.Final'
dependency 'org.hibernate:hibernate-validator:6.1.0.Final'
dependency 'org.hibernate:hibernate-core:5.2.18.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.4.13.Final'
dependency 'org.hibernate:hibernate-validator:6.1.2.Final'
dependency 'org.hsqldb:hsqldb:2.5.0'
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
dependency 'org.javassist:javassist:3.22.0-CR2'
@@ -193,21 +193,21 @@ dependencyManagement {
dependency 'org.mockito:mockito-core:3.0.0'
dependency 'org.objenesis:objenesis:2.6'
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
dependency 'org.opensaml:opensaml-core:3.4.3'
dependency 'org.opensaml:opensaml-saml-api:3.4.3'
dependency 'org.opensaml:opensaml-saml-impl:3.4.3'
dependency 'org.opensaml:opensaml-core:3.4.5'
dependency 'org.opensaml:opensaml-saml-api:3.4.5'
dependency 'org.opensaml:opensaml-saml-impl:3.4.5'
dependency 'org.ow2.asm:asm:6.2.1'
dependency 'org.reactivestreams:reactive-streams:1.0.1'
dependency 'org.reactivestreams:reactive-streams:1.0.3'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.36.0'
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
dependency 'org.skyscreamer:jsonassert:1.5.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.28'
dependency 'org.slf4j:jul-to-slf4j:1.7.28'
dependency 'org.slf4j:log4j-over-slf4j:1.7.28'
dependency 'org.slf4j:slf4j-api:1.7.28'
dependency 'org.slf4j:slf4j-nop:1.7.28'
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
dependency 'org.slf4j:jul-to-slf4j:1.7.30'
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
dependency 'org.slf4j:slf4j-api:1.7.30'
dependency 'org.slf4j:slf4j-nop:1.7.30'
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
dependency 'org.synchronoss.cloud:nio-multipart-parser:1.1.0'
@@ -28,7 +28,7 @@ public class BasicAuthenticationTests extends AbstractWebServerIntegrationTests
@Test
public void httpBasicWhenAuthenticationRequiredAndNotAuthenticatedThen401() throws Exception {
MockMvc mockMvc = createMockMvc("classpath:/spring/http-security-basic.xml", "classpath:/spring/in-memory-provider.xml", "classpath:/spring/testapp-servlet.xml");
mockMvc.perform(get("secure/index"))
mockMvc.perform(get("/secure/index"))
.andExpect(status().isUnauthorized());
}
@@ -47,7 +47,7 @@ public class ConcurrentSessionManagementTests extends AbstractWebServerIntegrati
MockMvc mockMvc = createMockMvc("classpath:/spring/http-security-concurrency.xml", "classpath:/spring/in-memory-provider.xml", "classpath:/spring/testapp-servlet.xml");
mockMvc.perform(get("secure/index").session(session1))
mockMvc.perform(get("/secure/index").session(session1))
.andExpect(status().is3xxRedirection());
MockHttpServletRequestBuilder login1 = login()
@@ -0,0 +1,144 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ldap.server;
import org.junit.After;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.ContextSource;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
/**
* Tests for {@link UnboundIdContainer}, specifically relating to LDIF file detection.
*
* @author Eleftheria Stein
*/
public class UnboundIdContainerLdifTests {
AnnotationConfigApplicationContext appCtx;
@After
public void closeAppContext() {
if (appCtx != null) {
appCtx.close();
appCtx = null;
}
}
@Test
public void unboundIdContainerWhenCustomLdifNameThenLdifLoaded() {
appCtx = new AnnotationConfigApplicationContext(CustomLdifConfig.class);
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
.getBean(ContextSource.class);
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
}
@Configuration
static class CustomLdifConfig {
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
"classpath:test-server.ldif");
@Bean
UnboundIdContainer ldapContainer() {
this.container.setPort(0);
return this.container;
}
@Bean
ContextSource contextSource(UnboundIdContainer container) {
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
+ container.getPort() + "/dc=springframework,dc=org");
}
@PreDestroy
void shutdown() {
this.container.stop();
}
}
@Test
public void unboundIdContainerWhenWildcardLdifNameThenLdifLoaded() {
appCtx = new AnnotationConfigApplicationContext(WildcardLdifConfig.class);
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
.getBean(ContextSource.class);
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
}
@Configuration
static class WildcardLdifConfig {
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
"classpath*:test-server.ldif");
@Bean
UnboundIdContainer ldapContainer() {
this.container.setPort(0);
return this.container;
}
@Bean
ContextSource contextSource(UnboundIdContainer container) {
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
+ container.getPort() + "/dc=springframework,dc=org");
}
@PreDestroy
void shutdown() {
this.container.stop();
}
}
@Test
public void unboundIdContainerWhenMalformedLdifThenException() {
try {
appCtx = new AnnotationConfigApplicationContext(MalformedLdifConfig.class);
failBecauseExceptionWasNotThrown(IllegalStateException.class);
} catch (Exception e) {
assertThat(e.getCause()).isInstanceOf(IllegalStateException.class);
assertThat(e.getMessage()).contains("Unable to load LDIF classpath:test-server-malformed.txt");
}
}
@Configuration
static class MalformedLdifConfig {
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
"classpath:test-server-malformed.txt");
@Bean
UnboundIdContainer ldapContainer() {
this.container.setPort(0);
return this.container;
}
@PreDestroy
void shutdown() {
this.container.stop();
}
}
}
@@ -0,0 +1,9 @@
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: subgroups
@@ -114,10 +114,10 @@ public class UnboundIdContainer implements InitializingBean, DisposableBean, Lif
private void importLdif(InMemoryDirectoryServer directoryServer) {
if (StringUtils.hasText(this.ldif)) {
Resource resource = this.context.getResource(this.ldif);
try {
if (resource.exists()) {
try (InputStream inputStream = resource.getInputStream()) {
Resource[] resources = this.context.getResources(this.ldif);
if (resources.length > 0 && resources[0].exists()) {
try (InputStream inputStream = resources[0].getInputStream()) {
directoryServer.importFromLDIF(false, new LDIFReader(inputStream));
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
/**
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
* that is capable of operating outside of a {@code ServerHttpRequest} context,
* e.g. in a scheduled/background thread and/or in the service-tier.
*
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
*
* @author Ankur Pathak
* @author Phil Clay
* @see ReactiveOAuth2AuthorizedClientManager
* @see ReactiveOAuth2AuthorizedClientProvider
* @see ReactiveOAuth2AuthorizedClientService
* @since 5.2.2
*/
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
implements ReactiveOAuth2AuthorizedClientManager {
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
/**
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
*
* @param clientRegistrationRepository the repository of client registrations
* @param authorizedClientService the authorized client service
*/
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
}
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
return createAuthorizationContext(authorizeRequest)
.flatMap(this::authorizeAndSave);
}
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
Authentication principal = authorizeRequest.getPrincipal();
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
.map(OAuth2AuthorizationContext::withAuthorizedClient)
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
.map(OAuth2AuthorizationContext::withAuthorizedClient)
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
.flatMap(contextBuilder -> this.contextAttributesMapper.apply(authorizeRequest)
.defaultIfEmpty(Collections.emptyMap())
.map(contextAttributes -> {
OAuth2AuthorizationContext.Builder builder = contextBuilder.principal(principal);
if (!contextAttributes.isEmpty()) {
builder = builder.attributes(attributes -> attributes.putAll(contextAttributes));
}
return builder.build();
}));
}
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
return this.authorizedClientProvider.authorize(authorizationContext)
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
authorizedClient,
authorizationContext.getPrincipal())
.thenReturn(authorizedClient))
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
}
/**
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
*
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client
*/
public void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
this.authorizedClientProvider = authorizedClientProvider;
}
/**
* Sets the {@code Function} used for mapping attribute(s) from the {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes
* to be associated to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
*
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
*/
public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
this.contextAttributesMapper = contextAttributesMapper;
}
/**
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
*/
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper =
new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
@Override
public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest) {
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-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.
@@ -73,7 +73,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken());
accessTokenResponse.getRefreshToken(),
accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
@@ -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);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
@@ -60,7 +59,7 @@ import java.util.Map;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
*/
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
Assert.notNull(userService, "userService cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
this.userService = userService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
OAuth2LoginAuthenticationToken loginAuthenticationToken =
(OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (authorizationCodeAuthentication.getAuthorizationExchange()
if (loginAuthenticationToken.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
// This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null;
}
OAuth2AccessTokenResponse accessTokenResponse;
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
OAuth2AuthorizationExchangeValidator.validate(
authorizationCodeAuthentication.getAuthorizationExchange());
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
accessTokenResponse.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
@@ -78,7 +78,6 @@ import java.util.Map;
*/
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
@@ -132,11 +131,6 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
@@ -80,7 +80,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
ReactiveAuthenticationManager {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
@@ -131,11 +130,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange());
@@ -146,9 +146,12 @@ public final class ClientRegistrations {
RequestEntity<Void> request = RequestEntity.get(uri).build();
Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
return withProviderConfiguration(metadata, issuer.toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
if (metadata.getUserInfoEndpointURI() != null) {
builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
}
return builder;
};
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -28,6 +28,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -121,9 +122,9 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
private static HttpServletRequest getHttpServletRequestOrDefault(Map<String, Object> attributes) {
HttpServletRequest servletRequest = (HttpServletRequest) attributes.get(HttpServletRequest.class.getName());
if (servletRequest == null) {
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (context != null) {
servletRequest = context.getRequest();
RequestAttributes context = RequestContextHolder.getRequestAttributes();
if (context instanceof ServletRequestAttributes) {
servletRequest = ((ServletRequestAttributes) context).getRequest();
}
}
return servletRequest;
@@ -132,9 +133,9 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
private static HttpServletResponse getHttpServletResponseOrDefault(Map<String, Object> attributes) {
HttpServletResponse servletResponse = (HttpServletResponse) attributes.get(HttpServletResponse.class.getName());
if (servletResponse == null) {
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (context != null) {
servletResponse = context.getResponse();
RequestAttributes context = RequestContextHolder.getRequestAttributes();
if (context instanceof ServletRequestAttributes) {
servletResponse = ((ServletRequestAttributes) context).getResponse();
}
}
return servletResponse;
@@ -99,15 +99,16 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React
private Mono<OAuth2AuthorizedClient> loadAuthorizedClient(String clientRegistrationId, Authentication principal, ServerWebExchange serverWebExchange) {
return Mono.justOrEmpty(serverWebExchange)
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
.flatMap(exchange -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, exchange));
}
private Mono<OAuth2AuthorizedClient> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, ServerWebExchange serverWebExchange) {
return Mono.justOrEmpty(serverWebExchange)
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
.flatMap(exchange -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, exchange)
.thenReturn(authorizedClient))
.defaultIfEmpty(authorizedClient);
.thenReturn(authorizedClient));
}
private static Mono<ServerWebExchange> currentServerWebExchange() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.FilterChain;
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
@@ -132,24 +138,39 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.shouldProcessAuthorizationResponse(request)) {
this.processAuthorizationResponse(request, response);
if (matchesAuthorizationResponse(request)) {
processAuthorizationResponse(request, response);
return;
}
filterChain.doFilter(request, response);
}
private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) {
private boolean matchesAuthorizationResponse(HttpServletRequest request) {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
return false;
}
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest == null) {
return false;
}
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
request.getServerPort(), request.getRequestURI(), null);
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
// Compare redirect_uri
UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build();
Set<Map.Entry<String, List<String>>> requestUriParameters = new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
Set<Map.Entry<String, List<String>>> redirectUriParameters = new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
// Remove the additional request parameters (if any) from the authorization response (request)
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
requestUriParameters.retainAll(redirectUriParameters);
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
return true;
}
return false;
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
String redirectUri = UrlUtils.buildFullRequestUrl(request);
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
} catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authorizationResponse.getRedirectUri())
.fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.isEmpty(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
@@ -206,7 +224,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
String redirectUrl = authorizationResponse.getRedirectUri();
String redirectUrl = authorizationRequest.getRedirectUri();
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) {
redirectUrl = savedRequest.getRedirectUrl();
@@ -22,6 +22,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
@@ -35,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
@@ -124,6 +126,17 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
.clientCredentials()
.password()
.build();
// gh-7544
if (authorizedClientRepository instanceof UnAuthenticatedServerOAuth2AuthorizedClientRepository) {
UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager =
new UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
(UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository);
unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return unauthenticatedAuthorizedClientManager;
}
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
@@ -266,7 +279,11 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
.clientCredentials(this::updateClientCredentialsProvider)
.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
.build();
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
if (this.authorizedClientManager instanceof UnAuthenticatedReactiveOAuth2AuthorizedClientManager) {
((UnAuthenticatedReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
} else {
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
}
}
private void updateClientCredentialsProvider(ReactiveOAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder builder) {
@@ -376,4 +393,52 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
.headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
.build();
}
private static class UnAuthenticatedReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
private UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientRepository = authorizedClientRepository;
}
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
Authentication principal = authorizeRequest.getPrincipal();
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
.switchIfEmpty(Mono.defer(() -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, null)))
.flatMap(authorizedClient -> {
// Re-authorize
return Mono.just(OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient).principal(principal).build())
.flatMap(this.authorizedClientProvider::authorize)
.flatMap(reauthorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(reauthorizedClient, principal, null).thenReturn(reauthorizedClient))
// Default to the existing authorizedClient if the client was not re-authorized
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
authorizeRequest.getAuthorizedClient() : authorizedClient);
})
.switchIfEmpty(Mono.deferWithContext(context ->
// Authorize
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
.flatMap(clientRegistration -> Mono.just(OAuth2AuthorizationContext.withClientRegistration(clientRegistration).principal(principal).build()))
.flatMap(this.authorizedClientProvider::authorize)
.flatMap(authorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, null).thenReturn(authorizedClient))
.subscriberContext(context)
));
}
private void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
this.authorizedClientProvider = authorizedClientProvider;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -36,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.client.ClientRequest;
@@ -389,15 +390,11 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
attrs.containsKey(HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
return;
}
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = null;
HttpServletResponse response = null;
if (context != null) {
request = context.getRequest();
response = context.getResponse();
RequestAttributes context = RequestContextHolder.getRequestAttributes();
if (context instanceof ServletRequestAttributes) {
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, ((ServletRequestAttributes) context).getRequest());
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, ((ServletRequestAttributes) context).getResponse());
}
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request);
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response);
}
private void populateDefaultAuthentication(Map<String, Object> attrs) {
@@ -137,13 +137,11 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
.switchIfEmpty(currentServerWebExchange());
return Mono.zip(defaultedRegistrationId, defaultedAuthentication, defaultedExchange)
.map(t3 -> {
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1()).principal(t3.getT2());
if (t3.getT3() != null) {
builder.attribute(ServerWebExchange.class.getName(), t3.getT3());
}
return builder.build();
});
.map(t3 -> OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1())
.principal(t3.getT2())
.attribute(ServerWebExchange.class.getName(), t3.getT3())
.build()
);
}
private Mono<Authentication> currentAuthentication() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -37,13 +37,20 @@ import org.springframework.security.web.server.authentication.ServerAuthenticati
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
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.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
* which handles the processing of the OAuth 2.0 Authorization Response.
@@ -165,10 +172,10 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.requiresAuthenticationMatcher.matches(exchange)
.filter( matchResult -> matchResult.isMatch())
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap(matchResult -> this.authenticationConverter.convert(exchange))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap( token -> authenticate(exchange, chain, token));
.flatMap(token -> authenticate(exchange, chain, token));
}
private Mono<Void> authenticate(ServerWebExchange exchange,
@@ -198,20 +205,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
}
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)
return Mono.just(exchange)
.filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
.flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
.flatMap(authorizationRequest ->
matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
}
private static Mono<ServerWebExchangeMatcher.MatchResult> matchesRedirectUri(
URI authorizationResponseUri, String authorizationRequestRedirectUri) {
UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build();
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build();
Set<Map.Entry<String, List<String>>> requestUriParameters =
new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
Set<Map.Entry<String, List<String>>> redirectUriParameters =
new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
// Remove the additional request parameters (if any) from the authorization response (request)
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
requestUriParameters.retainAll(redirectUriParameters);
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
@@ -103,14 +102,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
}
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
MultiValueMap<String, String> queryParams = exchange.getRequest()
.getQueryParams();
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.query(null)
.build()
.toUriString();
return OAuth2AuthorizationResponseUtils
.convert(queryParams, redirectUri);
.convert(exchange.getRequest().getQueryParams(), redirectUri);
}
}
@@ -0,0 +1,317 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import java.util.Map;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}.
*
* @author Ankur Pathak
* @author Phil Clay
*/
public class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManagerTests {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper;
private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
private ClientRegistration clientRegistration;
private Authentication principal;
private OAuth2AuthorizedClient authorizedClient;
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
private PublisherProbe<Void> saveAuthorizedClientProbe;
@SuppressWarnings("unchecked")
@Before
public void setup() {
this.clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
this.authorizedClientService = mock(ReactiveOAuth2AuthorizedClientService.class);
this.saveAuthorizedClientProbe = PublisherProbe.empty();
when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(this.saveAuthorizedClientProbe.mono());
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
this.contextAttributesMapper = mock(Function.class);
when(this.contextAttributesMapper.apply(any())).thenReturn(Mono.empty());
this.authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientService);
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
this.authorizedClientManager.setContextAttributesMapper(this.contextAttributesMapper);
this.clientRegistration = TestClientRegistrations.clientRegistration().build();
this.principal = new TestingAuthenticationToken("principal", "password");
this.authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.scopes("read", "write"), TestOAuth2RefreshTokens.refreshToken());
this.authorizationContextCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationContext.class);
}
@Test
public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(null, this.authorizedClientService))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientRegistrationRepository cannot be null");
}
@Test
public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(this.clientRegistrationRepository, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizedClientService cannot be null");
}
@Test
public void setAuthorizedClientProviderWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authorizedClientManager.setAuthorizedClientProvider(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizedClientProvider cannot be null");
}
@Test
public void setContextAttributesMapperWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authorizedClientManager.setContextAttributesMapper(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("contextAttributesMapper cannot be null");
}
@Test
public void authorizeWhenRequestIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authorizedClientManager.authorize(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizeRequest cannot be null");
}
@Test
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
String clientRegistrationId = "invalid-registration-id";
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
.principal(this.principal)
.build();
when(this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)).thenReturn(Mono.empty());
StepVerifier.create(this.authorizedClientManager.authorize(authorizeRequest))
.verifyError(IllegalArgumentException.class);
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenNotAuthorizedAndUnsupportedProviderThenNotAuthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientService.loadAuthorizedClient(
any(), any())).thenReturn(Mono.empty());
when(authorizedClientProvider.authorize(any())).thenReturn(Mono.empty());
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
StepVerifier.create(authorizedClient).verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isNull();
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService, never()).saveAuthorizedClient(
any(OAuth2AuthorizedClient.class), eq(this.principal));
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenNotAuthorizedAndSupportedProviderThenAuthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientService.loadAuthorizedClient(
any(), any())).thenReturn(Mono.empty());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(this.authorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isNull();
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService).saveAuthorizedClient(
eq(this.authorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientService.loadAuthorizedClient(
eq(this.clientRegistration.getRegistrationId()), eq(this.principal.getName()))).thenReturn(Mono.just(this.authorizedClient));
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(reauthorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService).saveAuthorizedClient(
eq(reauthorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@SuppressWarnings("unchecked")
@Test
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(this.authorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService, never()).saveAuthorizedClient(
any(OAuth2AuthorizedClient.class), eq(this.principal));
}
@SuppressWarnings("unchecked")
@Test
public void reauthorizeWhenSupportedProviderThenReauthorized() {
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(reauthorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService).saveAuthorizedClient(
eq(reauthorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@SuppressWarnings("unchecked")
@Test
public void reauthorizeWhenRequestAttributeScopeThenMappedToContext() {
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
.principal(this.principal)
.attribute(OAuth2ParameterNames.SCOPE, "read write")
.build();
this.authorizedClientManager.setContextAttributesMapper(new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(reauthorizedClient)
.verifyComplete();
verify(this.authorizedClientService).saveAuthorizedClient(
eq(reauthorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizationContext.getAttributes()).containsKey(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
assertThat(requestScopeAttribute).contains("read", "write");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-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.
@@ -16,6 +16,8 @@
package org.springframework.security.oauth2.client.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
@@ -98,19 +100,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthorizationException() {
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(
this.authorizationRequest, authorizationResponse);
assertThatThrownBy(() -> {
this.authenticationProvider.authenticate(
new OAuth2AuthorizationCodeAuthenticationToken(
this.clientRegistration, authorizationExchange));
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
}
@Test
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().refreshToken("refresh").build();
@@ -132,4 +121,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
assertThat(authenticationResult.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken());
}
// gh-5368
@Test
public void authenticateWhenAuthorizationSuccessResponseThenAdditionalParametersIncluded() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("param1", "value1");
additionalParameters.put("param2", "value2");
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
.build();
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest,
success().build());
OAuth2AuthorizationCodeAuthenticationToken authentication = (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider
.authenticate(
new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, authorizationExchange));
assertThat(authentication.getAdditionalParameters())
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,13 +81,6 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests {
.isInstanceOf(OAuth2AuthorizationException.class);
}
@Test
public void authenticateWhenRedirectUriNotEqualThenOAuth2AuthorizationException() {
this.authorizationRequest.redirectUri("https://example.org/notequal");
assertThatCode(() -> authenticate())
.isInstanceOf(OAuth2AuthorizationException.class);
}
@Test
public void authenticateWhenValidThenSuccess() {
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build()));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -151,20 +151,6 @@ public class OAuth2LoginAuthenticationProviderTests {
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
OAuth2AuthorizationResponse authorizationResponse =
success().redirectUri("https://example2.com").build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
this.authenticationProvider.authenticate(
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
@@ -186,19 +186,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
this.authenticationProvider.authenticate(
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
@@ -195,6 +195,14 @@ public class ClientRegistrationsTest {
assertThat(provider.getJwkSetUri()).isNull();
}
// gh-8187
@Test
public void issuerWhenResponseMissingUserInfoUriThenSuccess() throws Exception {
this.response.remove("userinfo_endpoint");
ClientRegistration registration = registration("").build();
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull();
}
@Test
public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception {
assertThat(registration("")).isNotNull();
@@ -65,6 +65,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
private MockServerWebExchange serverWebExchange;
private Context context;
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
private PublisherProbe<OAuth2AuthorizedClient> loadAuthorizedClientProbe;
private PublisherProbe<Void> saveAuthorizedClientProbe;
@SuppressWarnings("unchecked")
@@ -74,8 +75,9 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
when(this.clientRegistrationRepository.findByRegistrationId(
anyString())).thenReturn(Mono.empty());
this.authorizedClientRepository = mock(ServerOAuth2AuthorizedClientRepository.class);
this.loadAuthorizedClientProbe = PublisherProbe.empty();
when(this.authorizedClientRepository.loadAuthorizedClient(
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(Mono.empty());
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.loadAuthorizedClientProbe.mono());
this.saveAuthorizedClientProbe = PublisherProbe.empty();
when(this.authorizedClientRepository.saveAuthorizedClient(
any(OAuth2AuthorizedClient.class), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.saveAuthorizedClientProbe.mono());
@@ -131,6 +133,16 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
.hasMessage("authorizeRequest cannot be null");
}
@Test
public void authorizeWhenExchangeIsNullThenThrowIllegalArgumentException() {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
assertThatThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("serverWebExchange cannot be null");
}
@Test
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("invalid-registration-id")
@@ -162,7 +174,8 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizedClient).isNull();
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
this.loadAuthorizedClientProbe.assertWasSubscribed();
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
}
@SuppressWarnings("unchecked")
@@ -193,38 +206,14 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@Test
public void authorizeWhenNotAuthorizedAndSupportedProviderAndExchangeUnavailableThenAuthorizedButNotSaved() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientProvider.authorize(
any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isNull();
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizedClient).isSameAs(this.authorizedClient);
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
this.loadAuthorizedClientProbe = PublisherProbe.of(Mono.just(this.authorizedClient));
when(this.authorizedClientRepository.loadAuthorizedClient(
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(Mono.just(this.authorizedClient));
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(this.loadAuthorizedClientProbe.mono());
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
@@ -313,7 +302,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizedClient).isSameAs(this.authorizedClient);
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
}
@SuppressWarnings("unchecked")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -15,17 +15,9 @@
*/
package org.springframework.security.oauth2.client.web;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -50,13 +42,26 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.CollectionUtils;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
import static org.springframework.security.oauth2.core.TestOAuth2RefreshTokens.refreshToken;
@@ -131,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
HttpServletResponse response = mock(HttpServletResponse.class);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
@@ -142,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
@Test
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
request.setRequestURI(requestUri + "-no-match");
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestRedirectUriDoesNotMatchThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri);
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
authorizationResponse.setRequestURI(requestUri + "-no-match");
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
// gh-7963
@Test
public void doFilterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() throws Exception {
// 1) redirect_uri with query parameters
String requestUri = "/callback/client-1";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
FilterChain filterChain = mock(FilterChain.class);
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
this.filter.doFilter(authorizationResponse, response, filterChain);
verifyZeroInteractions(filterChain);
// 2) redirect_uri with query parameters AND authorization response additional parameters
Map<String, String> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("auth-param1", "value1");
additionalParameters.put("auth-param2", "value2");
response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
this.filter.doFilter(authorizationResponse, response, filterChain);
verifyZeroInteractions(filterChain);
}
// gh-7963
@Test
public void doFilterWhenAuthorizationRequestRedirectUriParametersDoesNotMatchThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
FilterChain filterChain = mock(FilterChain.class);
// 1) Parameter value
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.put("param2", "value8");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
// 2) Parameter order
parametersNotMatch = new LinkedHashMap<>();
parametersNotMatch.put("param2", "value2");
parametersNotMatch.put("param1", "value1");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(2)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
// 3) Parameter missing
parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.remove("param2");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(3)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestMatchThenAuthorizationRequestRemoved() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(authorizationResponse)).isNull();
}
@Test
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
when(this.authenticationManager.authenticate(any(Authentication.class)))
.thenThrow(new OAuth2AuthorizationException(error));
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
}
@Test
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
this.registration1.getRegistrationId(), this.principalName1);
@@ -241,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
}
@Test
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
}
@Test
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception {
public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
String requestUri = "/saved-request";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
RequestCache requestCache = new HttpSessionRequestCache();
requestCache.saveRequest(request, response);
requestUri = "/callback/client-1";
request.setRequestURI(requestUri);
request.setRequestURI("/callback/client-1");
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
@@ -284,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
}
@Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
AnonymousAuthenticationToken anonymousPrincipal =
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(anonymousPrincipal);
SecurityContextHolder.setContext(securityContext);
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), anonymousPrincipal, request);
this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false);
HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull();
@SuppressWarnings("unchecked")
@@ -325,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
}
@Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(securityContext); // null Authentication
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), null, request);
this.registration1.getRegistrationId(), null, authorizationResponse);
assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false);
HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull();
@SuppressWarnings("unchecked")
@@ -362,13 +393,51 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient);
}
private static MockHttpServletRequest createAuthorizationRequest(String requestUri) {
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
}
private static MockHttpServletRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
if (!CollectionUtils.isEmpty(parameters)) {
parameters.forEach(request::addParameter);
request.setQueryString(
parameters.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&")));
}
return request;
}
private static MockHttpServletRequest createAuthorizationResponse(MockHttpServletRequest authorizationRequest) {
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
}
private static MockHttpServletRequest createAuthorizationResponse(
MockHttpServletRequest authorizationRequest, Map<String, String> additionalParameters) {
MockHttpServletRequest authorizationResponse = new MockHttpServletRequest(
authorizationRequest.getMethod(), authorizationRequest.getRequestURI());
authorizationResponse.setServletPath(authorizationRequest.getRequestURI());
authorizationRequest.getParameterMap().forEach(authorizationResponse::addParameter);
authorizationResponse.addParameter(OAuth2ParameterNames.CODE, "code");
authorizationResponse.addParameter(OAuth2ParameterNames.STATE, "state");
additionalParameters.forEach(authorizationResponse::addParameter);
authorizationResponse.setQueryString(
authorizationResponse.getParameterMap().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()[0])
.collect(Collectors.joining("&")));
authorizationResponse.setSession(authorizationRequest.getSession());
return authorizationResponse;
}
private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
ClientRegistration registration) {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
OAuth2AuthorizationRequest authorizationRequest = request()
.additionalParameters(additionalParameters)
.redirectUri(request.getRequestURL().toString()).build();
.attributes(attributes)
.redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
@@ -57,6 +57,7 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
@@ -587,6 +588,43 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
verify(this.authorizedClientRepository).loadAuthorizedClient(eq(this.registration.getRegistrationId()), any(), eq(this.serverWebExchange));
}
// gh-7544
@Test
public void filterWhenClientCredentialsClientNotAuthorizedAndOutsideRequestContextThenGetNewToken() {
// Use UnAuthenticatedServerOAuth2AuthorizedClientRepository when operating outside of a request context
ServerOAuth2AuthorizedClientRepository unauthenticatedAuthorizedClientRepository = spy(new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
this.clientRegistrationRepository, unauthenticatedAuthorizedClientRepository);
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
.tokenType(OAuth2AccessToken.TokenType.BEARER)
.expiresIn(360)
.build();
when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(Mono.just(registration));
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(clientRegistrationId(registration.getRegistrationId()))
.build();
this.function.filter(request, this.exchange).block();
verify(unauthenticatedAuthorizedClientRepository).loadAuthorizedClient(any(), any(), any());
verify(this.clientCredentialsTokenResponseClient).getTokenResponse(any());
verify(unauthenticatedAuthorizedClientRepository).saveAuthorizedClient(any(), any(), any());
List<ClientRequest> requests = this.exchange.getRequests();
assertThat(requests).hasSize(1);
ClientRequest request1 = requests.get(0);
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
assertThat(getBody(request1)).isEmpty();
}
private Context serverWebExchange() {
return Context.of(ServerWebExchange.class, this.serverWebExchange);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -25,25 +25,28 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
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.util.CollectionUtils;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
/**
* @author Rob Winch
@@ -102,7 +105,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/"));
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete());
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
@@ -111,43 +114,161 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
@Test
public void filterWhenMatchThenAuthorizedClientSaved() {
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();
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any()))
.thenReturn(Mono.just(clientRegistration));
when(this.authenticationManager.authenticate(any())).thenReturn(
Mono.just(authenticated));
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback");
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(authorizationRequest));
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty());
ServerAuthenticationConverter converter = e -> authentication;
when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
this.authenticationManager, converter, this.authorizedClientRepository);
this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
MockServerHttpRequest request = MockServerHttpRequest
.get("/authorize/registration-id")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete());
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any());
}
// gh-7966
@Test
public void filterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any()))
.thenReturn(Mono.just(clientRegistration));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty());
when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
// 1) redirect_uri with query parameters
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback", parameters);
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verify(this.authenticationManager, times(1)).authenticate(any());
// 2) redirect_uri with query parameters AND authorization response additional parameters
Map<String, String> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("auth-param1", "value1");
additionalParameters.put("auth-param2", "value2");
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
exchange = MockServerWebExchange.from(authorizationResponse);
this.filter.filter(exchange, chain).block();
verify(this.authenticationManager, times(2)).authenticate(any());
}
// gh-7966
@Test
public void filterWhenAuthorizationRequestRedirectUriParametersNotMatchThenNotProcessed() {
String requestUri = "/authorization/callback";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest(requestUri, parameters);
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
// 1) Parameter value
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.put("param2", "value8");
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
// 2) Parameter order
parametersNotMatch = new LinkedHashMap<>();
parametersNotMatch.put("param2", "value2");
parametersNotMatch.put("param1", "value1");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
exchange = MockServerWebExchange.from(authorizationResponse);
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
// 3) Parameter missing
parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.remove("param2");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
exchange = MockServerWebExchange.from(authorizationResponse);
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
}
private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(
MockServerHttpRequest authorizationRequest, ClientRegistration registration) {
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
return request()
.attributes(attributes)
.redirectUri(authorizationRequest.getURI().toString())
.build();
}
private static MockServerHttpRequest createAuthorizationRequest(String requestUri) {
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
}
private static MockServerHttpRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
.get(requestUri);
if (!CollectionUtils.isEmpty(parameters)) {
parameters.forEach(builder::queryParam);
}
return builder.build();
}
private static MockServerHttpRequest createAuthorizationResponse(MockServerHttpRequest authorizationRequest) {
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
}
private static MockServerHttpRequest createAuthorizationResponse(
MockServerHttpRequest authorizationRequest, Map<String, String> additionalParameters) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
.get(authorizationRequest.getURI().toString());
builder.queryParam(OAuth2ParameterNames.CODE, "code");
builder.queryParam(OAuth2ParameterNames.STATE, "state");
additionalParameters.forEach(builder::queryParam);
builder.cookies(authorizationRequest.getCookies());
return builder.build();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -23,6 +23,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@@ -376,29 +377,34 @@ public final class OAuth2AuthorizationRequest implements Serializable {
private String buildAuthorizationRequestUri() {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, this.clientId);
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, encodeQueryParam(this.responseType.getValue()));
parameters.set(OAuth2ParameterNames.CLIENT_ID, encodeQueryParam(this.clientId));
if (!CollectionUtils.isEmpty(this.scopes)) {
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(this.scopes, " "));
encodeQueryParam(StringUtils.collectionToDelimitedString(this.scopes, " ")));
}
if (this.state != null) {
parameters.set(OAuth2ParameterNames.STATE, this.state);
parameters.set(OAuth2ParameterNames.STATE, encodeQueryParam(this.state));
}
if (this.redirectUri != null) {
parameters.set(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri);
parameters.set(OAuth2ParameterNames.REDIRECT_URI, encodeQueryParam(this.redirectUri));
}
if (!CollectionUtils.isEmpty(this.additionalParameters)) {
this.additionalParameters.forEach((k, v) -> parameters.set(k, v.toString()));
this.additionalParameters.forEach((k, v) ->
parameters.set(encodeQueryParam(k), encodeQueryParam(v.toString())));
}
return UriComponentsBuilder.fromHttpUrl(this.authorizationUri)
.queryParams(parameters)
.encode(StandardCharsets.UTF_8)
.build()
.toUriString();
}
// Encode query parameter value according to RFC 3986
private static String encodeQueryParam(String value) {
return UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8);
}
private LinkedHashSet<String> toLinkedHashSet(String... scope) {
LinkedHashSet<String> result = new LinkedHashSet<>();
Collections.addAll(result, scope);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -43,6 +43,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.stream.Collectors;
/**
* A {@link HttpMessageConverter} for an {@link OAuth2AccessTokenResponse OAuth 2.0 Access Token Response}.
@@ -55,8 +56,8 @@ import java.util.HashMap;
public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {};
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -80,10 +81,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
throws HttpMessageNotReadableException {
try {
// gh-6463
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked")
Map<String, String> tokenResponseParameters = (Map<String, String>) this.jsonMessageConverter.read(
Map<String, Object> tokenResponseParameters = (Map<String, Object>) this.jsonMessageConverter.read(
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
return this.tokenResponseConverter.convert(tokenResponseParameters);
return this.tokenResponseConverter.convert(
tokenResponseParameters.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().toString())));
} catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
ex.getMessage(), ex, inputMessage);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -34,6 +34,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}.
@@ -46,8 +47,8 @@ import java.util.Map;
public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {};
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -69,10 +70,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
throws HttpMessageNotReadableException {
try {
// gh-8157
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked")
Map<String, String> errorParameters = (Map<String, String>) this.jsonMessageConverter.read(
Map<String, Object> errorParameters = (Map<String, Object>) this.jsonMessageConverter.read(
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
return this.errorConverter.convert(errorParameters);
return this.errorConverter.convert(
errorParameters.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> String.valueOf(entry.getValue()))));
} catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
ex.getMessage(), ex, inputMessage);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -307,4 +307,38 @@ public class OAuth2AuthorizationRequestTests {
"response_type=code&client_id=client-id&state=state&" +
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
}
@Test
public void buildWhenAuthorizationUriIncludesEscapedQueryParameterThenAuthorizationRequestUrlIncludesIt() {
OAuth2AuthorizationRequest authorizationRequest =
TestOAuth2AuthorizationRequests.request()
.authorizationUri(AUTHORIZATION_URI +
"?claims=%7B%22userinfo%22%3A%7B%22email_verified%22%3A%7B%22essential%22%3Atrue%7D%7D%7D").build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://provider.com/oauth2/authorize?" +
"claims=%7B%22userinfo%22%3A%7B%22email_verified%22%3A%7B%22essential%22%3Atrue%7D%7D%7D&" +
"response_type=code&client_id=client-id&state=state&" +
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
}
@Test
public void buildWhenNonAsciiAdditionalParametersThenProperlyEncoded() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("item amount", "19.95" + '\u20ac');
additionalParameters.put("item name", "H" + '\u00c5' + "M" + '\u00d6');
additionalParameters.put('\u00e2' + "ge", "4" + '\u00bd');
OAuth2AuthorizationRequest authorizationRequest =
TestOAuth2AuthorizationRequests.request()
.additionalParameters(additionalParameters)
.build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://example.com/login/oauth/authorize?" +
"response_type=code&client_id=client-id&state=state&" +
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id&" +
"item%20amount=19.95%E2%82%AC&%C3%A2ge=4%C2%BD&item%20name=H%C3%85M%C3%96");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -96,6 +96,39 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests {
}
// gh-6463
@Test
public void readInternalWhenSuccessfulTokenResponseWithObjectThenReadOAuth2AccessTokenResponse() {
String tokenResponse = "{\n" +
" \"access_token\": \"access-token-1234\",\n" +
" \"token_type\": \"bearer\",\n" +
" \"expires_in\": 3600,\n" +
" \"scope\": \"read write\",\n" +
" \"refresh_token\": \"refresh-token-1234\",\n" +
" \"custom_object_1\": {\"name1\": \"value1\"},\n" +
" \"custom_object_2\": [\"value1\", \"value2\"],\n" +
" \"custom_parameter_1\": \"custom-value-1\",\n" +
" \"custom_parameter_2\": \"custom-value-2\"\n" +
"}\n";
MockClientHttpResponse response = new MockClientHttpResponse(
tokenResponse.getBytes(), HttpStatus.OK);
OAuth2AccessTokenResponse accessTokenResponse = this.messageConverter.readInternal(
OAuth2AccessTokenResponse.class, response);
assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBeforeOrEqualTo(Instant.now().plusSeconds(3600));
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write");
assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234");
assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(
entry("custom_object_1", "{name1=value1}"),
entry("custom_object_2", "[value1, value2]"),
entry("custom_parameter_1", "custom-value-1"),
entry("custom_parameter_2", "custom-value-2"));
}
@Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter tokenResponseConverter = mock(Converter.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -78,6 +78,25 @@ public class OAuth2ErrorHttpMessageConverterTests {
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
}
// gh-8157
@Test
public void readInternalWhenErrorResponseWithObjectThenReadOAuth2Error() throws Exception {
String errorResponse = "{\n" +
" \"error\": \"unauthorized_client\",\n" +
" \"error_description\": \"The client is not authorized\",\n" +
" \"error_codes\": [65001],\n" +
" \"error_uri\": \"https://tools.ietf.org/html/rfc6749#section-5.2\"\n" +
"}\n";
MockClientHttpResponse response = new MockClientHttpResponse(
errorResponse.getBytes(), HttpStatus.BAD_REQUEST);
OAuth2Error oauth2Error = this.messageConverter.readInternal(OAuth2Error.class, response);
assertThat(oauth2Error.getErrorCode()).isEqualTo("unauthorized_client");
assertThat(oauth2Error.getDescription()).isEqualTo("The client is not authorized");
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
}
@Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter errorConverter = mock(Converter.class);
@@ -133,7 +133,7 @@ public class NimbusOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
public OAuth2AuthenticatedPrincipal introspect(String token) {
RequestEntity<?> requestEntity = this.requestEntityConverter.convert(token);
if (requestEntity == null) {
throw new OAuth2IntrospectionException("Provided token [" + token + "] isn't active");
throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity");
}
ResponseEntity<String> responseEntity = makeRequest(requestEntity);
@@ -143,7 +143,7 @@ public class NimbusOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
// relying solely on the authorization server to validate this token (not checking 'exp', for example)
if (!introspectionSuccessResponse.isActive()) {
throw new OAuth2IntrospectionException("Provided token [" + token + "] isn't active");
throw new OAuth2IntrospectionException("Provided token isn't active");
}
return convertClaimsSet(introspectionSuccessResponse);
@@ -154,7 +154,7 @@ public class NimbusReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
private void validate(String token, TokenIntrospectionSuccessResponse response) {
// relying solely on the authorization server to validate this token (not checking 'exp', for example)
if (!response.isActive()) {
throw new OAuth2IntrospectionException("Provided token [" + token + "] isn't active");
throw new OAuth2IntrospectionException("Provided token isn't active");
}
}
@@ -168,7 +168,7 @@ public class NimbusOpaqueTokenIntrospectorTests {
assertThatCode(() -> introspectionClient.introspect("token"))
.isInstanceOf(OAuth2IntrospectionException.class)
.extracting("message")
.containsExactly("Provided token [token] isn't active");
.containsExactly("Provided token isn't active");
}
@Test
@@ -142,7 +142,7 @@ public class NimbusReactiveOpaqueTokenIntrospectorTests {
assertThatCode(() -> introspectionClient.introspect("token").block())
.isInstanceOf(OAuth2IntrospectionException.class)
.extracting("message")
.containsExactly("Provided token [token] isn't active");
.containsExactly("Provided token isn't active");
}
@Test
@@ -178,7 +178,7 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse);
String username = getUsername(token, assertion);
return new Saml2Authentication(
() -> username, token.getSaml2Response(),
new SimpleSaml2AuthenticatedPrincipal(username), token.getSaml2Response(),
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))
);
} catch (Saml2AuthenticationException e) {
@@ -0,0 +1,28 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import org.springframework.security.core.AuthenticatedPrincipal;
/**
* Saml2 representation of an {@link AuthenticatedPrincipal}.
*
* @author Clement Stoquart
* @since 5.2.2
*/
public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal {
}
@@ -0,0 +1,39 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import java.io.Serializable;
/**
* Default implementation of a {@link Saml2AuthenticatedPrincipal}.
*
* @author Clement Stoquart
* @since 5.2.2
*/
class SimpleSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal, Serializable {
private final String name;
SimpleSaml2AuthenticatedPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
@@ -25,6 +25,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
@@ -94,13 +95,17 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
String xml = this.authenticationRequestFactory.createAuthenticationRequest(authNRequest);
String encoded = encode(deflate(xml));
String relayState = request.getParameter("RelayState");
String redirect = UriComponentsBuilder
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(relyingParty.getIdpWebSsoUrl())
.queryParam("SAMLRequest", UriUtils.encode(encoded, StandardCharsets.ISO_8859_1))
.queryParam("RelayState", UriUtils.encode(relayState, StandardCharsets.ISO_8859_1))
.queryParam("SAMLRequest", UriUtils.encode(encoded, StandardCharsets.ISO_8859_1));
if (StringUtils.hasText(relayState)) {
uriBuilder.queryParam("RelayState", UriUtils.encode(relayState, StandardCharsets.ISO_8859_1));
}
return uriBuilder
.build(true)
.toUriString();
return redirect;
}
private Saml2AuthenticationRequest createAuthenticationRequest(RelyingPartyRegistration relyingParty, HttpServletRequest request) {
@@ -16,6 +16,10 @@
package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import org.springframework.security.core.Authentication;
import org.hamcrest.BaseMatcher;
@@ -346,6 +350,28 @@ public class OpenSamlAuthenticationProviderTests {
provider.authenticate(token);
}
@Test
public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException {
Response response = response(recipientUri, idpEntityId);
Assertion assertion = defaultAssertion();
signXmlObject(
assertion,
assertingPartyCredentials(),
recipientEntityId
);
EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials());
response.getEncryptedAssertions().add(encryptedAssertion);
token = responseXml(response, idpEntityId);
Saml2Authentication authentication = (Saml2Authentication) provider.authenticate(token);
// the following code will throw an exception if authentication isn't serializable
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
objectOutputStream.writeObject(authentication);
objectOutputStream.flush();
}
private Assertion defaultAssertion() {
return assertion(
username,
@@ -0,0 +1,30 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication;
import org.junit.Assert;
import org.junit.Test;
public class SimpleSaml2AuthenticatedPrincipalTests {
@Test
public void createSimpleSaml2AuthenticatedPrincipal() {
SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user");
Assert.assertEquals("user", principal.getName());
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.servlet.filter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.security.saml2.provider.service.servlet.filter.TestSaml2SigningCredentials.signingCredential;
public class Saml2WebSsoAuthenticationRequestFilterTests {
private Saml2WebSsoAuthenticationRequestFilter filter;
private RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class);
private MockHttpServletRequest request;
private HttpServletResponse response;
private MockFilterChain filterChain;
@Before
public void setup() {
filter = new Saml2WebSsoAuthenticationRequestFilter(repository);
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setPathInfo("/saml2/authenticate/registration-id");
filterChain = new MockFilterChain();
}
@Test
public void createSamlRequestRedirectUrlAndReturnUrlWithoutRelayState() throws ServletException, IOException {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration
.withRegistrationId("registration-id")
.remoteIdpEntityId("idp-entity-id")
.idpWebSsoUrl("sso-url")
.assertionConsumerServiceUrlTemplate("template")
.credentials(c -> c.add(signingCredential()))
.build();
when(repository.findByRegistrationId("registration-id"))
.thenReturn(relyingPartyRegistration);
filter.doFilterInternal(request, response, filterChain);
Assert.assertFalse(response.getHeader("Location").contains("RelayState="));
}
@Test
public void createSamlRequestRedirectUrlAndReturnUrlWithRelayState() throws ServletException, IOException {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration
.withRegistrationId("registration-id")
.remoteIdpEntityId("idp-entity-id")
.idpWebSsoUrl("sso-url")
.assertionConsumerServiceUrlTemplate("template")
.credentials(c -> c.add(signingCredential()))
.build();
when(repository.findByRegistrationId("registration-id"))
.thenReturn(relyingPartyRegistration);
request.setParameter("RelayState", "my-relay-state");
filter.doFilterInternal(request, response, filterChain);
Assert.assertTrue(response.getHeader("Location").contains("RelayState=my-relay-state"));
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.servlet.filter;
import java.io.ByteArrayInputStream;
import java.security.KeyException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.opensaml.security.crypto.KeySupport;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.credentials.Saml2X509Credential;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION;
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING;
final class TestSaml2SigningCredentials {
static Saml2X509Credential signingCredential() {
return new Saml2X509Credential(idpPrivateKey(), idpCertificate(), SIGNING, DECRYPTION);
}
private static X509Certificate certificate(String cert) {
ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes());
try {
return (X509Certificate) CertificateFactory
.getInstance("X.509")
.generateCertificate(certBytes);
}
catch (CertificateException e) {
throw new Saml2Exception(e);
}
}
private static PrivateKey privateKey(String key) {
try {
return KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]);
}
catch (KeyException e) {
throw new Saml2Exception(e);
}
}
private static X509Certificate idpCertificate() {
return certificate("-----BEGIN CERTIFICATE-----\n"
+ "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n"
+ "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n"
+ "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n"
+ "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n"
+ "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n"
+ "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n"
+ "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n"
+ "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n"
+ "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n"
+ "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n"
+ "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n"
+ "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n"
+ "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n"
+ "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n"
+ "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n"
+ "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n"
+ "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n"
+ "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n"
+ "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n"
+ "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n"
+ "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n"
+ "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n"
+ "-----END CERTIFICATE-----\n");
}
private static PrivateKey idpPrivateKey() {
return privateKey("-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4cn62E1xLqpN3\n"
+ "4PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZX\n"
+ "W+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHE\n"
+ "fDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7h\n"
+ "Z6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/T\n"
+ "Xy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7\n"
+ "I+J5lS8VAgMBAAECggEBAKyxBlIS7mcp3chvq0RF7B3PHFJMMzkwE+t3pLJcs4cZ\n"
+ "nezh/KbREfP70QjXzk/llnZCvxeIs5vRu24vbdBm79qLHqBuHp8XfHHtuo2AfoAQ\n"
+ "l4h047Xc/+TKMivnPQ0jX9qqndKDLqZDf5wnbslDmlskvF0a/MjsLU0TxtOfo+dB\n"
+ "t55FW11cGqxZwhS5Gnr+cbw3OkHz23b9gEOt9qfwPVepeysbmm9FjU+k4yVa7rAN\n"
+ "xcbzVb6Y7GCITe2tgvvEHmjB9BLmWrH3mZ3Af17YU/iN6TrpPd6Sj3QoS+2wGtAe\n"
+ "HbUs3CKJu7bIHcj4poal6Kh8519S+erJTtqQ8M0ZiEECgYEA43hLYAPaUueFkdfh\n"
+ "9K/7ClH6436CUH3VdizwUXi26fdhhV/I/ot6zLfU2mgEHU22LBECWQGtAFm8kv0P\n"
+ "zPn+qjaR3e62l5PIlSYbnkIidzoDZ2ztu4jF5LgStlTJQPteFEGgZVl5o9DaSZOq\n"
+ "Yd7G3XqXuQ1VGMW58G5FYJPtA1cCgYEAz5TPUtK+R2KXHMjUwlGY9AefQYRYmyX2\n"
+ "Tn/OFgKvY8lpAkMrhPKONq7SMYc8E9v9G7A0dIOXvW7QOYSapNhKU+np3lUafR5F\n"
+ "4ZN0bxZ9qjHbn3AMYeraKjeutHvlLtbHdIc1j3sxe/EzltRsYmiqLdEBW0p6hwWg\n"
+ "tyGhYWVyaXMCgYAfDOKtHpmEy5nOCLwNXKBWDk7DExfSyPqEgSnk1SeS1HP5ctPK\n"
+ "+1st6sIhdiVpopwFc+TwJWxqKdW18tlfT5jVv1E2DEnccw3kXilS9xAhWkfwrEvf\n"
+ "V5I74GydewFl32o+NZ8hdo9GL1I8zO1rIq/et8dSOWGuWf9BtKu/vTGTTQKBgFxU\n"
+ "VjsCnbvmsEwPUAL2hE/WrBFaKocnxXx5AFNt8lEyHtDwy4Sg1nygGcIJ4sD6koQk\n"
+ "RdClT3LkvR04TAiSY80bN/i6ZcPNGUwSaDGZEWAIOSWbkwZijZNFnSGOEgxZX/IG\n"
+ "yd39766vREEMTwEeiMNEOZQ/dmxkJm4OOVe25cLdAoGACOtPnq1Fxay80UYBf4rQ\n"
+ "+bJ9yX1ulB8WIree1hD7OHSB2lRHxrVYWrglrTvkh63Lgx+EcsTV788OsvAVfPPz\n"
+ "BZrn8SdDlQqalMxUBYEFwnsYD3cQ8yOUnijFVC4xNcdDv8OIqVgSk4KKxU5AshaA\n"
+ "xk6Mox+u8Cc2eAK12H13i+8=\n"
+ "-----END PRIVATE KEY-----\n");
}
}
@@ -255,42 +255,6 @@ public class OAuth2LoginApplicationTests {
assertThat(errorElement.asText()).contains("authorization_request_not_found");
}
@Test
public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
HtmlPage page = this.webClient.getPage("/");
URL loginPageUrl = page.getBaseURL();
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google");
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, clientRegistration);
assertThat(clientAnchorElement).isNotNull();
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
URI.create(response.getResponseHeaderValue("Location"))).build();
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
String code = "auth-code";
String state = URLDecoder.decode(params.get(OAuth2ParameterNames.STATE), "UTF-8");
String redirectUri = URLDecoder.decode(params.get(OAuth2ParameterNames.REDIRECT_URI), "UTF-8");
redirectUri += "-invalid";
String authorizationResponseUri =
UriComponentsBuilder.fromHttpUrl(redirectUri)
.queryParam(OAuth2ParameterNames.CODE, code)
.queryParam(OAuth2ParameterNames.STATE, state)
.build().encode().toUriString();
page = this.webClient.getPage(new URL(authorizationResponseUri));
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
HtmlElement errorElement = page.getBody().getFirstByXPath("div");
assertThat(errorElement).isNotNull();
assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
}
private void assertLoginPage(HtmlPage page) {
assertThat(page.getTitleText()).isEqualTo("Please sign in");
@@ -66,7 +66,7 @@ class JettyCasService extends Server {
String password = System.getProperty('javax.net.ssl.trustStorePassword','password')
SslContextFactory sslContextFactory = new SslContextFactory();
SslContextFactory sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(getTrustStore());
sslContextFactory.setKeyStorePassword(password);
sslContextFactory.setKeyManagerPassword(password);
@@ -0,0 +1,14 @@
releasenotes:
sections:
- title: "New Features"
emoji: ":star:"
labels: ["enhancement"]
- title: "Bug Fixes"
emoji: ":beetle:"
labels: ["bug", "regression"]
- title: "Dependency Upgrades"
emoji: ":hammer:"
labels: ["dependency-upgrade"]
- title: "Non-passive"
emoji: ":rewind:"
labels: ["breaks-passivity"]
@@ -43,7 +43,7 @@ final class WithMockUserSecurityContextFactory implements
.username() : withUser.value();
if (username == null) {
throw new IllegalArgumentException(withUser
+ " cannot have null username on both username and value properites");
+ " cannot have null username on both username and value properties");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
@@ -563,6 +563,6 @@ public class SwitchUserFilter extends GenericFilterBean
}
private static RequestMatcher createMatcher(String pattern) {
return new AntPathRequestMatcher(pattern, null, true, new UrlPathHelper());
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
}
}
@@ -87,6 +87,10 @@ public class BasicAuthenticationConverter implements AuthenticationConverter {
return null;
}
if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
throw new BadCredentialsException("Empty basic authentication token");
}
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
@@ -17,6 +17,7 @@
package org.springframework.security.web.authentication.www;
import java.io.IOException;
import java.nio.charset.Charset;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -276,6 +277,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
public void setCredentialsCharset(String credentialsCharset) {
Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty");
this.credentialsCharset = credentialsCharset;
this.authenticationConverter.setCredentialsCharset(Charset.forName(credentialsCharset));
}
protected String getCredentialsCharset(HttpServletRequest httpRequest) {
@@ -19,11 +19,11 @@ package org.springframework.security.web.server.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
/**
* Delegates to a collection of {@link ServerAuthenticationSuccessHandler} implementations.
@@ -42,10 +42,8 @@ public class DelegatingServerAuthenticationSuccessHandler implements ServerAuthe
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange,
Authentication authentication) {
List<Mono<Void>> results = new ArrayList<>();
for (ServerAuthenticationSuccessHandler delegate : delegates) {
results.add(delegate.onAuthenticationSuccess(exchange, authentication));
}
return Mono.when(results);
return Flux.fromIterable(this.delegates)
.concatMap(delegate -> delegate.onAuthenticationSuccess(exchange, authentication))
.then();
}
}
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
@@ -48,12 +49,8 @@ public class DelegatingServerLogoutHandler implements ServerLogoutHandler {
@Override
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
List<Mono<Void>> results = new ArrayList<>();
for (ServerLogoutHandler delegate : delegates) {
if (delegate != null) {
results.add(delegate.logout(exchange, authentication));
}
}
return Mono.when(results);
return Flux.fromIterable(this.delegates)
.concatMap(delegate -> delegate.logout(exchange, authentication))
.then();
}
}
@@ -15,13 +15,12 @@
*/
package org.springframework.security.web.server.header;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Combines multiple {@link ServerHttpHeadersWriter} instances into a single instance.
@@ -42,10 +41,8 @@ public class CompositeServerHttpHeadersWriter implements ServerHttpHeadersWriter
@Override
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
List<Mono<Void>> results = new ArrayList<>();
for (ServerHttpHeadersWriter writer : writers) {
results.add(writer.writeHttpHeaders(exchange));
}
return Mono.when(results);
return Flux.fromIterable(this.writers)
.concatMap(w -> w.writeHttpHeaders(exchange))
.then();
}
}
@@ -106,7 +106,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
+ " <body>\n"
+ " <div class=\"container\">\n"
+ formLogin(queryParams, csrfTokenHtmlInput)
+ oauth2LoginLinks(contextPath, this.oauth2AuthenticationUrlToClientName)
+ oauth2LoginLinks(queryParams, contextPath, this.oauth2AuthenticationUrlToClientName)
+ " </div>\n"
+ " </body>\n"
+ "</html>";
@@ -135,12 +135,14 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
+ " </form>\n";
}
private static String oauth2LoginLinks(String contextPath, Map<String, String> oauth2AuthenticationUrlToClientName) {
private static String oauth2LoginLinks(MultiValueMap<String, String> queryParams, String contextPath, Map<String, String> oauth2AuthenticationUrlToClientName) {
if (oauth2AuthenticationUrlToClientName.isEmpty()) {
return "";
}
boolean isError = queryParams.containsKey("error");
StringBuilder sb = new StringBuilder();
sb.append("<div class=\"container\"><h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
sb.append(createError(isError));
sb.append("<table class=\"table table-striped\">\n");
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
sb.append(" <tr><td>");
@@ -42,7 +42,6 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -80,7 +79,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
private LogoutHandler logoutHandler;
private List<LogoutHandler> logoutHandlers;
HttpServlet3RequestFactory(String rolePrefix) {
this.rolePrefix = rolePrefix;
@@ -144,7 +143,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
* {@link HttpServletRequest#logout()}.
*/
public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) {
this.logoutHandler = CollectionUtils.isEmpty(logoutHandlers) ? null : new CompositeLogoutHandler(logoutHandlers);
this.logoutHandlers = logoutHandlers;
}
/**
@@ -244,8 +243,8 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
@Override
public void logout() throws ServletException {
LogoutHandler handler = HttpServlet3RequestFactory.this.logoutHandler;
if (handler == null) {
List<LogoutHandler> handlers = HttpServlet3RequestFactory.this.logoutHandlers;
if (CollectionUtils.isEmpty(handlers)) {
HttpServlet3RequestFactory.this.logger.debug(
"logoutHandlers is null, so allowing original HttpServletRequest to handle logout");
super.logout();
@@ -253,7 +252,9 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
}
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
handler.logout(this, this.response, authentication);
for (LogoutHandler handler : handlers) {
handler.logout(this, this.response, authentication);
}
}
private boolean isAuthenticated() {
@@ -108,7 +108,7 @@ public final class AntPathRequestMatcher
*
* @param pattern the ant pattern to use for matching
* @param httpMethod the HTTP method. The {@code matches} method will return false if
* the incoming request doesn't doesn't have the same method.
* the incoming request doesn't have the same method.
* @param caseSensitive true if the matcher should consider case, else false
* @param urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest
*/
@@ -16,11 +16,16 @@
package org.springframework.security.web.authentication.switchuser;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.FilterChain;
import org.junit.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AccountExpiredException;
@@ -42,8 +47,10 @@ import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import javax.servlet.FilterChain;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests
@@ -75,6 +82,7 @@ public class SwitchUserFilterTests {
request.setScheme("http");
request.setServerName("localhost");
request.setRequestURI("/login/impersonate");
request.setMethod("POST");
return request;
}
@@ -125,6 +133,20 @@ public class SwitchUserFilterTests {
assertThat(filter.requiresExitUser(request)).isFalse();
}
@Test
// gh-4183
public void requiresExitUserWhenGetThenDoesNotMatch() {
SwitchUserFilter filter = new SwitchUserFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("localhost");
request.setRequestURI("/login/impersonate");
request.setMethod("GET");
assertThat(filter.requiresExitUser(request)).isFalse();
}
@Test
public void requiresExitUserWhenMatcherThenWorks() {
SwitchUserFilter filter = new SwitchUserFilter();
@@ -159,6 +181,20 @@ public class SwitchUserFilterTests {
assertThat(filter.requiresSwitchUser(request)).isFalse();
}
@Test
// gh-4183
public void requiresSwitchUserWhenGetThenDoesNotMatch() {
SwitchUserFilter filter = new SwitchUserFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("localhost");
request.setRequestURI("/login/impersonate");
request.setMethod("GET");
assertThat(filter.requiresSwitchUser(request)).isFalse();
}
@Test
public void requiresSwitchUserWhenMatcherThenWorks() {
SwitchUserFilter filter = new SwitchUserFilter();
@@ -111,4 +111,12 @@ public class BasicAuthenticationConverterTests {
assertThat(authentication.getName()).isEqualTo("rod");
assertThat(authentication.getCredentials()).isEqualTo("");
}
@Test(expected = BadCredentialsException.class)
public void requestWhenEmptyBasicAuthorizationHeaderTokenThenError() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Basic ");
converter.convert(request);
}
}
@@ -16,13 +16,14 @@
package org.springframework.security.web.authentication.www;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.Mockito.*;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.junit.After;
@@ -40,6 +41,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.web.util.WebUtils;
import java.nio.charset.StandardCharsets;
/**
* Tests {@link BasicAuthenticationFilter}.
*
@@ -320,4 +323,121 @@ public class BasicAuthenticationFilterTests {
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void doFilterWhenTokenAndFilterCharsetMatchDefaultThenAuthenticated() throws Exception {
SecurityContextHolder.clearContext();
UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü");
rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1"));
manager = mock(AuthenticationManager.class);
when(manager.authenticate(rodRequest)).thenReturn(rod);
when(manager.authenticate(not(eq(rodRequest)))).thenThrow(new BadCredentialsException(""));
filter = new BasicAuthenticationFilter(manager, new BasicAuthenticationEntryPoint());
String token = "rod:äöü";
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.UTF_8))));
request.setServletPath("/some_file.html");
MockHttpServletResponse response = new MockHttpServletResponse();
// Test
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
FilterChain chain = mock(FilterChain.class);
filter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class));
assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod");
assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("äöü");
}
@Test
public void doFilterWhenTokenAndFilterCharsetMatchNonDefaultThenAuthenticated() throws Exception {
SecurityContextHolder.clearContext();
UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü");
rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1"));
manager = mock(AuthenticationManager.class);
when(manager.authenticate(rodRequest)).thenReturn(rod);
when(manager.authenticate(not(eq(rodRequest)))).thenThrow(new BadCredentialsException(""));
filter = new BasicAuthenticationFilter(manager, new BasicAuthenticationEntryPoint());
filter.setCredentialsCharset("ISO-8859-1");
String token = "rod:äöü";
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.ISO_8859_1))));
request.setServletPath("/some_file.html");
MockHttpServletResponse response = new MockHttpServletResponse();
// Test
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
FilterChain chain = mock(FilterChain.class);
filter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class));
assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod");
assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("äöü");
}
@Test
public void doFilterWhenTokenAndFilterCharsetDoNotMatchThenUnauthorized() throws Exception {
SecurityContextHolder.clearContext();
UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü");
rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1"));
manager = mock(AuthenticationManager.class);
when(manager.authenticate(rodRequest)).thenReturn(rod);
when(manager.authenticate(not(eq(rodRequest)))).thenThrow(new BadCredentialsException(""));
filter = new BasicAuthenticationFilter(manager, new BasicAuthenticationEntryPoint());
filter.setCredentialsCharset("ISO-8859-1");
String token = "rod:äöü";
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.UTF_8))));
request.setServletPath("/some_file.html");
MockHttpServletResponse response = new MockHttpServletResponse();
// Test
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
FilterChain chain = mock(FilterChain.class);
filter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
verify(chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class));
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
}
@Test
public void requestWhenEmptyBasicAuthorizationHeaderTokenThenUnauthorized() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Basic ");
request.setServletPath("/some_file.html");
request.setSession(new MockHttpSession());
final MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
filter.doFilter(request, response, chain);
verify(chain, never()).doFilter(any(ServletRequest.class),
any(ServletResponse.class));
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
assertThat(response.getStatus()).isEqualTo(401);
}
}
@@ -16,10 +16,6 @@
package org.springframework.security.web.server.authentication;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -27,9 +23,19 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import reactor.core.publisher.Mono;
import reactor.test.publisher.PublisherProbe;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
* @since 5.1
@@ -88,4 +94,26 @@ public class DelegatingServerAuthenticationSuccessHandlerTests {
this.delegate1Result.assertWasSubscribed();
this.delegate2Result.assertWasSubscribed();
}
@Test
public void onAuthenticationSuccessSequential() throws Exception {
AtomicBoolean slowDone = new AtomicBoolean();
CountDownLatch latch = new CountDownLatch(1);
ServerAuthenticationSuccessHandler slow = (exchange, authentication) ->
Mono.delay(Duration.ofMillis(100))
.doOnSuccess(__ -> slowDone.set(true))
.then();
ServerAuthenticationSuccessHandler second = (exchange, authentication) ->
Mono.fromRunnable(() -> {
latch.countDown();
assertThat(slowDone.get())
.describedAs("ServerAuthenticationSuccessHandler should be executed sequentially")
.isTrue();
});
DelegatingServerAuthenticationSuccessHandler handler = new DelegatingServerAuthenticationSuccessHandler(slow, second);
handler.onAuthenticationSuccess(this.exchange, this.authentication).block();
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
}
}
@@ -16,6 +16,7 @@
package org.springframework.security.web.server.authentication.logout;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*;
@@ -28,9 +29,14 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import reactor.core.publisher.Mono;
import reactor.test.publisher.PublisherProbe;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author Eric Deandrea
@@ -98,4 +104,26 @@ public class DelegatingServerLogoutHandlerTests {
this.delegate1Result.assertWasSubscribed();
this.delegate2Result.assertWasSubscribed();
}
@Test
public void logoutSequential() throws Exception {
AtomicBoolean slowDone = new AtomicBoolean();
CountDownLatch latch = new CountDownLatch(1);
ServerLogoutHandler slow = (exchange, authentication) ->
Mono.delay(Duration.ofMillis(100))
.doOnSuccess(__ -> slowDone.set(true))
.then();
ServerLogoutHandler second = (exchange, authentication) ->
Mono.fromRunnable(() -> {
latch.countDown();
assertThat(slowDone.get())
.describedAs("ServerLogoutHandler should be executed sequentially")
.isTrue();
});
DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(slow, second);
handler.logout(this.exchange, this.authentication).block();
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
}
}
@@ -15,11 +15,6 @@
*/
package org.springframework.security.web.server.header;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,10 +23,19 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
*
* @author Rob Winch
@@ -55,7 +59,6 @@ public class CompositeServerHttpHeadersWriterTests {
@Test
public void writeHttpHeadersWhenErrorNoErrorThenError() {
when(writer1.writeHttpHeaders(exchange)).thenReturn(Mono.error(new RuntimeException()));
when(writer2.writeHttpHeaders(exchange)).thenReturn(Mono.empty());
Mono<Void> result = writer.writeHttpHeaders(exchange);
@@ -64,13 +67,11 @@ public class CompositeServerHttpHeadersWriterTests {
.verify();
verify(writer1).writeHttpHeaders(exchange);
verify(writer2).writeHttpHeaders(exchange);
}
@Test
public void writeHttpHeadersWhenErrorErrorThenError() {
when(writer1.writeHttpHeaders(exchange)).thenReturn(Mono.error(new RuntimeException()));
when(writer2.writeHttpHeaders(exchange)).thenReturn(Mono.error(new RuntimeException()));
Mono<Void> result = writer.writeHttpHeaders(exchange);
@@ -79,7 +80,6 @@ public class CompositeServerHttpHeadersWriterTests {
.verify();
verify(writer1).writeHttpHeaders(exchange);
verify(writer2).writeHttpHeaders(exchange);
}
@Test
@@ -96,4 +96,26 @@ public class CompositeServerHttpHeadersWriterTests {
verify(writer1).writeHttpHeaders(exchange);
verify(writer2).writeHttpHeaders(exchange);
}
@Test
public void writeHttpHeadersSequential() throws Exception {
AtomicBoolean slowDone = new AtomicBoolean();
CountDownLatch latch = new CountDownLatch(1);
ServerHttpHeadersWriter slow = exchange ->
Mono.delay(Duration.ofMillis(100))
.doOnSuccess(__ -> slowDone.set(true))
.then();
ServerHttpHeadersWriter second = exchange ->
Mono.fromRunnable(() -> {
latch.countDown();
assertThat(slowDone.get())
.describedAs("ServerLogoutHandler should be executed sequentially")
.isTrue();
});
CompositeServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(slow, second);
writer.writeHttpHeaders(this.exchange).block();
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
}
}
@@ -67,7 +67,7 @@ public class HttpHeaderWriterWebFilterTests {
verify(writer, never()).writeHttpHeaders(any());
result.getExchange().getResponse().setComplete();
result.getExchange().getResponse().setComplete().block();
verify(writer).writeHttpHeaders(any());
}