1
0
mirror of synced 2026-08-06 02:08:01 +00:00

Compare commits

...

65 Commits

Author SHA1 Message Date
Eleftheria Stein 37568780a1 Release 5.1.11.RELEASE 2020-06-03 17:48:40 -04:00
Josh Cummings d80b1865a5 Polish setAllowedHostnames
Added JavaDoc to method, including @since attribute

Issue gh-4310
2020-06-03 08:54:06 -06:00
Eddú Meléndez 52c80c78e5 Add support for allowedHostnames in StrictHttpFirewall
Introduce a new method `setAllowedHostnames` which perform the validation
against untrusted hostnames.

Fixes gh-4310
2020-06-03 08:53:59 -06:00
Eleftheria Stein ded83cc1b3 Update to jaxb-impl 2.3.3
Fixes gh-8634
2020-06-02 18:45:14 -04:00
Eleftheria Stein 7a1833c1df Update to okhttp 3.12.12
Fixes gh-8635
2020-06-02 18:45:14 -04:00
Eleftheria Stein e6630ea0f1 Update to mockwebserver 3.12.12
Fixes gh-8633
2020-06-02 18:45:14 -04:00
Eleftheria Stein 2400e8fde2 Update to Spring Boot 2.1.14.RELEASE
Fixes gh-8632
2020-06-02 18:45:14 -04:00
justmehyp f05d70a4a5 Remove unused field 'digester' in Md4PasswordEncoder
`private Digester digester;`  defined in Md4PasswordEncoder is never used. So remove it.

Closes gh-8553
2020-05-21 11:21:40 -05:00
Maksim Vinogradov 8bb4e72aff Prevent StackOverflowError for AccessControlEntryImpl.hashCode
Getting StackOverflowError when invoke AclImpl.hashCode because of
cross-references between AclImpl and AccessControlEntryImpl

Remove from AccessControlEntryImpl.hashCode method invocation of
acl.hashCode

fixes gh-5401
2020-05-21 10:08:56 -05:00
Rob Winch f58a262eb4 Revert "Create the CSRF token on the bounded elactic scheduler"
Reactor did not add bounded elastic until a later version.

This reverts commit c0154f2315.
2020-05-18 11:10:41 -05:00
cbornet c0154f2315 Create the CSRF token on the bounded elactic scheduler
The CSRF token is created with a call to UUID.randomUUID which is blocking.
This change ensures this blocking call is done on the bounded elastic scheduler which supports blocking calls.

Fixes gh-8128
2020-05-18 11:07:08 -05:00
Artyom Tarynin cea2b556d6 Update AntPathRequestMatcher.java
Fixes gh-8512
2020-05-14 10:36:30 -04:00
Dávid Kovács faa02e8bc0 Document NoOpPasswordEncoder will not be removed
This commit adds extension to deprecation notice.

Fixes gh-8506
2020-05-13 12:56:22 -05:00
Rob Winch d9f57492d4 Fix non-standard HTTP method for CsrfWebFilter
Closes gh-8452
2020-05-12 13:21:22 -05:00
Eleftheria Stein b007fdc333 Next development version 2020-05-06 16:27:44 -04:00
Eleftheria Stein 16c350a7bc Release 5.1.10.RELEASE 2020-05-06 15:55:12 -04:00
Eleftheria Stein 6d6a22eda1 Update to org.powermock 2.0.7
Closes gh-8475
2020-05-06 09:23:32 -04:00
Eleftheria Stein 1e957e96b9 Update to Spring Data Lovelace-SR17
Closes gh-8474
2020-05-06 09:22:47 -04:00
Eleftheria Stein 9986a33177 Update to Reactor Californium-SR18
Closes gh-8473
2020-05-06 09:21:59 -04:00
Eleftheria Stein ecf58c9549 Update to Spring Framework 5.1.15.RELEASE
Closes gh-8472
2020-05-06 09:21:22 -04:00
Eleftheria Stein 497ee84537 Update to GAE 1.9.80
Closes gh-8470
2020-05-06 09:20:39 -04:00
Eleftheria Stein 30a68b6c10 Clean up Javadoc
Fixes gh-8480
2020-05-05 17:34:29 -04:00
Rob Winch aa118466e5 Add ROLE_INFRASTRUCTURE to infrastructure beans
Closes gh-8407
2020-04-27 09:19:58 -05:00
Dávid Kovács f98db2d866 ActiveDirectoryLdapAuthenticationProvider uses InternalAuthenticationServiceException
Closes gh-2884
2020-04-24 10:25:09 -05:00
Souphorn 52af3998c8 Fix typo with correct capitalization
Closes gh-8406
2020-04-17 14:19:58 -04:00
Rob Winch 1b91259412 Logout defaults to use Global SecurityContextServerLogoutHandler
Closes gh-8375
2020-04-13 16:46:00 -05:00
Rob Winch 9d3ea58bb1 Fix example in javadoc of FilterChainProxy
Closes gh-8344
2020-04-08 09:14:32 -05:00
Alan Czajkowski c2296b0376 BCryptPasswordEncoder rawPassword cannot be null
Closes gh-8317
2020-04-07 13:47:37 -05:00
hotire 24d251f232 Fix typo in Javadoc of ServerHttpSecurity#hasAuthority
Closes gh-8336
2020-04-06 14:20:57 -05:00
Eleftheria Stein 18610eab6f Fix HttpSecurity Javadoc
Fixes gh-4404
2020-04-02 11:50:13 -04:00
Eleftheria Stein 1964cfdfaa Next Development Version 2020-04-01 16:24:58 -04:00
Eleftheria Stein 0ae7ceef70 Release 5.1.9.RELEASE 2020-04-01 14:37:37 -04:00
Eleftheria Stein b3c7f0a79f Update to httpclient 4.5.12
Fixes gh-8294
2020-04-01 11:19:49 -04:00
Eleftheria Stein 4df4f9a63d Update to hibernate-validator 6.0.19.Final
Fixes gh-8293
2020-04-01 11:19:09 -04:00
Eleftheria Stein 9d25c2d2f8 Update to reactive-streams 1.0.3
Fixes gh-8292
2020-04-01 11:18:35 -04:00
Eleftheria Stein 94b116c8c8 Update to hibernate-core 5.2.18.Final
Fixes gh-8291
2020-04-01 11:18:05 -04:00
Eleftheria Stein 3c6f08ede0 Update to groovy 2.4.19
Fixes gh-8290
2020-04-01 11:04:53 -04:00
Eleftheria Stein 9a94234dae Update to unboundid-ldapsdk 4.0.14
Fixes gh-8289
2020-04-01 11:03:25 -04:00
Eleftheria Stein 017044bf93 Update to okhttp 3.12.10
Fixes gh-8288
2020-04-01 11:02:52 -04:00
Eleftheria Stein ae99097723 Update to mockwebserver 3.12.10
Fixes gh-8287
2020-04-01 11:01:53 -04:00
Eleftheria Stein acb7e63cf7 Update to org.powermock 2.0.6
Fixes gh-8286
2020-04-01 11:01:12 -04:00
Eleftheria Stein 0256439a50 Update to Spring Boot 2.1.13.RELEASE
Fixes gh-8285
2020-04-01 11:00:28 -04:00
Eleftheria Stein c8f72a1b87 Update to GAE 1.9.79
Fixes gh-8284
2020-04-01 11:00:00 -04:00
Eleftheria Stein 332e8ce07a Update to Spring Data Lovelace-SR16
Fixes gh-8282
2020-04-01 10:59:11 -04:00
Eleftheria Stein 7aea459535 Update to Reactor Californium-SR17
Fixes gh-8283
2020-04-01 10:58:44 -04:00
Eleftheria Stein c91389ff8b Update to Spring Framework 5.1.14.RELEASE
Fixes gh-8281
2020-04-01 10:58:04 -04:00
Rob Winch 6de345b972 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 21:55:27 -05:00
Ruby Hartono 19f08cbedb 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:15:08 -04:00
Josh Cummings f82e435aaf Add Missing Import
Restored an import that was lost during a backport

Issue gh-4183
2020-03-27 15:07:57 -06:00
Josh Cummings a44225d334 SwitchUserFilter Defaults to POST
Fixes gh-4183
2020-03-27 14:42:21 -06:00
Eleftheria Stein 5decfb1ece Update Encryptors documentation
Fixes gh-8208
2020-03-27 10:48:45 -04:00
Joe Grandja a24d67375b oauth2Login WebFlux does not auto-redirect for XHR request
Fixes gh-8118
2020-03-26 05:27:43 -04:00
Joe Grandja 3a46ba8a85 OAuth2ErrorHttpMessageConverter handles JSON object parameters
Fixes gh-8157
2020-03-24 14:57:52 -04:00
Erik van Paassen a4851095df 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:37:01 -06:00
Markus Engelbrecht 633e5b85b4 Fix typo 'properites' in documentation
Fixes gh-8095
2020-03-11 11:02:05 -06:00
Josh Cummings bfa5830e3d Update to Jetty 9.4.27
Fixes gh-7507
2020-03-09 10:10:04 -06:00
Pim Moerenhout e43fab518c Fix typo in log message. 2020-03-04 10:33:36 -07:00
AmitB c9f676739b Fix typo in AntPathRequestMatcher contructor comment
Closes gh-8042
2020-03-02 07:20:50 -06:00
Josh Cummings b7ce65b284 Register Authentication Provider in Init Phase
Fixes gh-8031
2020-02-29 13:07:39 -07:00
Joe Grandja 67d561b5f7 OAuth2AccessTokenResponseHttpMessageConverter handles JSON object parameters
Fixes gh-6463
2020-02-24 16:10:18 -05:00
LeeHainie 4dd2b1dfe1 Remove unwanted code
Remove unwanted code
2020-02-20 12:33:55 -07:00
Joe Grandja aa28a0b453 OAuth2AuthorizationCodeGrantWebFilter matches on query parameters
Fixes gh-7966
2020-02-10 16:09:57 -05:00
Joe Grandja 8145cb557b OAuth2AuthorizationCodeGrantFilter matches on query parameters
Fixes gh-7963
2020-02-10 05:41:34 -05:00
Joe Grandja ef4109358a Add release-notes-sections.yml 2020-02-05 15:19:57 -05:00
Joe Grandja 6d6fd09665 Next Development Version 2020-02-05 10:04:48 -05:00
54 changed files with 1049 additions and 390 deletions
@@ -1,5 +1,5 @@
/* /*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -131,10 +131,9 @@ public class AccessControlEntryImpl implements AccessControlEntry,
@Override @Override
public int hashCode() { public int hashCode() {
int result = this.acl.hashCode(); int result = this.permission.hashCode();
result = 31 * result + this.permission.hashCode();
result = 31 * result + (this.id != null ? this.id.hashCode() : 0); result = 31 * result + (this.id != null ? this.id.hashCode() : 0);
result = 31 * result + this.sid.hashCode(); result = 31 * result + (this.sid.hashCode());
result = 31 * result + (this.auditFailure ? 1 : 0); result = 31 * result + (this.auditFailure ? 1 : 0);
result = 31 * result + (this.auditSuccess ? 1 : 0); result = 31 * result + (this.auditSuccess ? 1 : 0);
result = 31 * result + (this.granting ? 1 : 0); result = 31 * result + (this.granting ? 1 : 0);
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -560,6 +560,25 @@ public class AclImplTests {
childAcl.setParent(changeParentAcl); childAcl.setParent(changeParentAcl);
} }
@Test
public void hashCodeWithoutStackOverFlow() throws Exception {
//given
Sid sid = new PrincipalSid("pSid");
ObjectIdentity oid = new ObjectIdentityImpl("type", 1);
AclAuthorizationStrategy authStrategy = new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("role"));
PermissionGrantingStrategy grantingStrategy = new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
AclImpl acl = new AclImpl(oid, 1L, authStrategy, grantingStrategy, null, null, false, sid);
AccessControlEntryImpl ace = new AccessControlEntryImpl(1L, acl, sid, BasePermission.READ, true, true, true);
Field fieldAces = FieldUtils.getField(AclImpl.class, "aces");
fieldAces.setAccessible(true);
List<AccessControlEntryImpl> aces = (List<AccessControlEntryImpl>) fieldAces.get(acl);
aces.add(ace);
//when - then none StackOverFlowError been raised
ace.hashCode();
}
// ~ Inner Classes // ~ Inner Classes
// ================================================================================================== // ==================================================================================================
@@ -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. * CAS artifactParameterName.
* *
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl. * @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
@@ -16,8 +16,10 @@
package org.springframework.security.config.annotation.configuration; package org.springframework.security.config.annotation.configuration;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -34,9 +36,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
* @since 3.2 * @since 3.2
*/ */
@Configuration @Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ObjectPostProcessorConfiguration { public class ObjectPostProcessorConfiguration {
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ObjectPostProcessor<Object> objectPostProcessor( public ObjectPostProcessor<Object> objectPostProcessor(
AutowireCapableBeanFactory beanFactory) { AutowireCapableBeanFactory beanFactory) {
return new AutowireBeanFactoryObjectPostProcessor(beanFactory); return new AutowireBeanFactoryObjectPostProcessor(beanFactory);
@@ -31,8 +31,10 @@ import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
@@ -83,6 +85,7 @@ import org.springframework.util.Assert;
* @see EnableGlobalMethodSecurity * @see EnableGlobalMethodSecurity
*/ */
@Configuration @Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware { implements ImportAware, SmartInitializingSingleton, BeanFactoryAware {
private static final Log logger = LogFactory private static final Log logger = LogFactory
@@ -15,14 +15,18 @@
*/ */
package org.springframework.security.config.annotation.method.configuration; package org.springframework.security.config.annotation.method.configuration;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource; import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
@Configuration @Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class Jsr250MetadataSourceConfiguration { class Jsr250MetadataSourceConfiguration {
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() { public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() {
return new Jsr250MethodSecurityMetadataSource(); return new Jsr250MethodSecurityMetadataSource();
} }
@@ -49,6 +49,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
} }
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DelegatingMethodSecurityMetadataSource methodMetadataSource() { public DelegatingMethodSecurityMetadataSource methodMetadataSource() {
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory( ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
new DefaultMethodSecurityExpressionHandler()); new DefaultMethodSecurityExpressionHandler());
@@ -69,6 +70,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
} }
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() { public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
return new DefaultMethodSecurityExpressionHandler(); return new DefaultMethodSecurityExpressionHandler();
} }
@@ -586,6 +586,7 @@ public final class HttpSecurity extends
/** /**
* Allows restricting access based upon the {@link HttpServletRequest} using * Allows restricting access based upon the {@link HttpServletRequest} using
* {@link RequestMatcher} implementations (i.e. via URL patterns).
* *
* <h2>Example Configurations</h2> * <h2>Example Configurations</h2>
* *
@@ -723,7 +724,7 @@ public final class HttpSecurity extends
* } * }
* </pre> * </pre>
* *
* @return the {@link ServletApiConfigurer} for further customizations * @return the {@link CsrfConfigurer} for further customizations
* @throws Exception * @throws Exception
*/ */
public CsrfConfigurer<HttpSecurity> csrf() throws Exception { public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
@@ -171,7 +171,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
/** /**
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is * Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites * {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
* set. * set.
* *
* @param successHandler the {@link AuthenticationSuccessHandler}. * @param successHandler the {@link AuthenticationSuccessHandler}.
@@ -160,6 +160,25 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
@Override @Override
public void init(H http) throws Exception { public void init(H http) throws Exception {
if ( this.jwtConfigurer == null ) {
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
"a jwk set uri by doing http.oauth2ResourceServer().jwt().jwkSetUri(uri), or wire a " +
"JwtDecoder instance by doing http.oauth2ResourceServer().jwt().decoder(decoder), or " +
"expose a JwtDecoder instance as a bean and do http.oauth2ResourceServer().jwt().");
}
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
this.jwtConfigurer.getJwtAuthenticationConverter();
JwtAuthenticationProvider provider =
new JwtAuthenticationProvider(decoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
provider = postProcess(provider);
http.authenticationProvider(provider);
registerDefaultAccessDeniedHandler(http); registerDefaultAccessDeniedHandler(http);
registerDefaultEntryPoint(http); registerDefaultEntryPoint(http);
registerDefaultCsrfOverride(http); registerDefaultCsrfOverride(http);
@@ -179,25 +198,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
filter = postProcess(filter); filter = postProcess(filter);
http.addFilter(filter); http.addFilter(filter);
if ( this.jwtConfigurer == null ) {
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
"a jwk set uri by doing http.oauth2ResourceServer().jwt().jwkSetUri(uri), or wire a " +
"JwtDecoder instance by doing http.oauth2ResourceServer().jwt().decoder(decoder), or " +
"expose a JwtDecoder instance as a bean and do http.oauth2ResourceServer().jwt().");
}
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
this.jwtConfigurer.getJwtAuthenticationConverter();
JwtAuthenticationProvider provider =
new JwtAuthenticationProvider(decoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
provider = postProcess(provider);
http.authenticationProvider(provider);
} }
public class JwtConfigurer { public class JwtConfigurer {
@@ -208,7 +208,7 @@ public class GlobalMethodSecurityBeanDefinitionParser implements BeanDefinitionP
pc.registerBeanComponent(new BeanComponentDefinition( pc.registerBeanComponent(new BeanComponentDefinition(
expressionHandler, expressionHandlerRef)); expressionHandler, expressionHandlerRef));
logger.info("Expressions were enabled for method security but no SecurityExpressionHandler was configured. " logger.info("Expressions were enabled for method security but no SecurityExpressionHandler was configured. "
+ "All hasPermision() expressions will evaluate to false."); + "All hasPermission() expressions will evaluate to false.");
} }
BeanDefinitionBuilder expressionPreAdviceBldr = BeanDefinitionBuilder BeanDefinitionBuilder expressionPreAdviceBldr = BeanDefinitionBuilder
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -621,20 +621,56 @@ public class ServerHttpSecurity {
authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error")); authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error"));
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( setDefaultEntryPoints(http);
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")));
}
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); 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().getOrDefault("X-Requested-With", Collections.emptyList()).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 ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() { private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}"); return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
} }
@@ -1467,7 +1503,7 @@ public class ServerHttpSecurity {
/** /**
* Require a specific authority. * Require a specific authority.
* @param authority the authority to require (i.e. "USER" woudl require authority of "USER"). * @param authority the authority to require (i.e. "USER" would require authority of "USER").
* @return the {@link AuthorizeExchangeSpec} to configure * @return the {@link AuthorizeExchangeSpec} to configure
*/ */
public AuthorizeExchangeSpec hasAuthority(String authority) { public AuthorizeExchangeSpec hasAuthority(String authority) {
@@ -2376,7 +2412,9 @@ public class ServerHttpSecurity {
*/ */
public final class LogoutSpec { public final class LogoutSpec {
private LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); private LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(new SecurityContextServerLogoutHandler())); private final SecurityContextServerLogoutHandler DEFAULT_LOGOUT_HANDLER = new SecurityContextServerLogoutHandler();
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(this.DEFAULT_LOGOUT_HANDLER));
/** /**
* Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler} * Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler}
@@ -2440,6 +2478,10 @@ public class ServerHttpSecurity {
} }
private Optional<ServerLogoutHandler> createLogoutHandler() { private Optional<ServerLogoutHandler> createLogoutHandler() {
ServerSecurityContextRepository securityContextRepository = ServerHttpSecurity.this.securityContextRepository;
if (securityContextRepository != null) {
this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository);
}
if (this.logoutHandlers.isEmpty()) { if (this.logoutHandlers.isEmpty()) {
return Optional.empty(); return Optional.empty();
} }
@@ -298,6 +298,18 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
} }
// gh-8031
@Test
public void getWhenAnonymousDisabledThenAllows() throws Exception {
this.spring.register(JwtDecoderConfig.class, AnonymousDisabledConfig.class).autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
when(decoder.decode(anyString())).thenReturn(JWT);
this.mvc.perform(get("/authenticated")
.with(bearerToken("token")))
.andExpect(status().isNotFound());
}
@Test @Test
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized() public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
throws Exception { throws Exception {
@@ -652,7 +664,8 @@ public class OAuth2ResourceServerConfigurerTests {
@Test @Test
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() { public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire()) assertThatCode(() -> this.spring
.register(JwtDecoderConfig.class, MultipleBearerTokenResolverBeansConfig.class).autowire())
.isInstanceOf(BeanCreationException.class) .isInstanceOf(BeanCreationException.class)
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class); .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
} }
@@ -1097,6 +1110,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 @EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter { static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
@@ -21,6 +21,7 @@ import org.openqa.selenium.WebDriver;
import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder; import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -117,4 +118,45 @@ public class LogoutSpecTests {
.assertAt() .assertAt()
.assertLogout(); .assertLogout();
} }
@Test
public void logoutWhenCustomSecurityContextRepositoryThenLogsOut() {
WebSessionServerSecurityContextRepository repository = new WebSessionServerSecurityContextRepository();
repository.setSpringSecurityContextAttrName("CUSTOM_CONTEXT_ATTR");
SecurityWebFilterChain securityWebFilter = this.http
.securityContextRepository(repository)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.formLogin()
.and()
.logout()
.and()
.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();
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
.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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -26,8 +26,10 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
@@ -61,10 +63,12 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.time.Duration; import java.time.Duration;
@@ -79,6 +83,8 @@ public class OAuth2LoginTests {
@Rule @Rule
public final SpringTestRule spring = new SpringTestRule(); public final SpringTestRule spring = new SpringTestRule();
private WebTestClient client;
@Autowired @Autowired
private WebFilterChainProxy springSecurity; private WebFilterChainProxy springSecurity;
@@ -94,6 +100,14 @@ public class OAuth2LoginTests {
.clientSecret("secret") .clientSecret("secret")
.build(); .build();
@Autowired
public void setApplicationContext(ApplicationContext context) {
if (context.getBeanNamesForType(WebHandler.class).length > 0) {
this.client = WebTestClient.bindToApplicationContext(context)
.build();
}
}
@Test @Test
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() { public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire(); this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
@@ -140,6 +154,22 @@ public class OAuth2LoginTests {
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize"); 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 @EnableWebFluxSecurity
static class OAuth2LoginWithSingleClientRegistrations { static class OAuth2LoginWithSingleClientRegistrations {
@Bean @Bean
@@ -17,7 +17,6 @@ package org.springframework.security.access.expression;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.PermissionEvaluator;
@@ -158,7 +157,6 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
private Set<String> getAuthoritySet() { private Set<String> getAuthoritySet() {
if (roles == null) { if (roles == null) {
roles = new HashSet<>();
Collection<? extends GrantedAuthority> userAuthorities = authentication Collection<? extends GrantedAuthority> userAuthorities = authentication
.getAuthorities(); .getAuthorities();
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
* &lt;/property&gt; * &lt;/property&gt;
* </pre> * </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 * "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 * gets configured, create a subclass of this and override the
* {@link #configureJaas(Resource)} method. * {@link #configureJaas(Resource)} method.
@@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found
JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority
JdbcDaoImpl.notFound=User {0} not found JdbcDaoImpl.notFound=User {0} not found
LdapAuthenticationProvider.badCredentials=Bad credentials LdapAuthenticationProvider.badCredentials=Bad credentials
LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
LdapAuthenticationProvider.credentialsExpired=User credentials have expired LdapAuthenticationProvider.credentialsExpired=User credentials have expired
LdapAuthenticationProvider.disabled=User is disabled LdapAuthenticationProvider.disabled=User is disabled
LdapAuthenticationProvider.expired=User account has expired LdapAuthenticationProvider.expired=User account has expired
@@ -65,6 +65,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
} }
public String encode(CharSequence rawPassword) { public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt; String salt;
if (strength > 0) { if (strength > 0) {
if (random != null) { if (random != null) {
@@ -81,6 +85,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
} }
public boolean matches(CharSequence rawPassword, String encodedPassword) { public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) { if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password"); logger.warn("Empty encoded password");
return false; return false;
@@ -1,5 +1,5 @@
/* /*
* Copyright 2011-2016 the original author or authors. * Copyright 2011-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -32,16 +32,13 @@ public class Encryptors {
* (Password-Based Key Derivation Function #2). Salts the password to prevent * (Password-Based Key Derivation Function #2). Salts the password to prevent
* dictionary attacks against the key. The provided salt is expected to be * dictionary attacks against the key. The provided salt is expected to be
* hex-encoded; it should be random and at least 8 bytes in length. Also applies a * 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 * random 16-byte initialization vector to ensure each encrypted message will be
* unique. Requires Java 6. * unique. Requires Java 6.
* *
* @param password the password used to generate the encryptor's secret key; should * @param password the password used to generate the encryptor's secret key; should
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* key * key
*
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
* (instead of GCM)
*/ */
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) { public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt, return new AesBytesEncryptor(password.toString(), salt,
@@ -53,13 +50,21 @@ public class Encryptors {
* Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation * Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation
* Function #2). Salts the password to prevent dictionary attacks against the key. The * Function #2). Salts the password to prevent dictionary attacks against the key. The
* provided salt is expected to be hex-encoded; it should be random and at least 8 * 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 * bytes in length. Also applies a random 16-byte initialization vector to ensure each
* encrypted message will be unique. Requires Java 6. * 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 * @param password the password used to generate the encryptor's secret key; should
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* key * key
*
* @see #stronger(CharSequence, CharSequence), which uses the significatly more secure
* GCM (instead of CBC)
*/ */
public static BytesEncryptor standard(CharSequence password, CharSequence salt) { public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt, return new AesBytesEncryptor(password.toString(), salt,
@@ -100,7 +105,10 @@ public class Encryptors {
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* secret key * secret key
* @deprecated This encryptor is not secure. Instead, look to your data store for a
* mechanism to query encrypted data.
*/ */
@Deprecated
public static TextEncryptor queryableText(CharSequence password, CharSequence salt) { public static TextEncryptor queryableText(CharSequence password, CharSequence salt) {
return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(), return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
salt)); salt));
@@ -83,8 +83,6 @@ public class Md4PasswordEncoder implements PasswordEncoder {
private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator(); private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
private boolean encodeHashAsBase64; private boolean encodeHashAsBase64;
private Digester digester;
public void setEncodeHashAsBase64(boolean encodeHashAsBase64) { public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
this.encodeHashAsBase64 = encodeHashAsBase64; this.encodeHashAsBase64 = encodeHashAsBase64;
@@ -26,7 +26,8 @@ package org.springframework.security.crypto.password;
* @deprecated This PasswordEncoder is not secure. Instead use an * @deprecated This PasswordEncoder is not secure. Instead use an
* adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
* SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
* password upgrades. * password upgrades. There are no plans to remove this support. It is deprecated to indicate that
* this is a legacy implementation and using it is considered insecure.
*/ */
@Deprecated @Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder { public final class NoOpPasswordEncoder implements PasswordEncoder {
@@ -92,4 +92,15 @@ public class BCryptPasswordEncoderTests {
assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse(); assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse();
} }
@Test(expected = IllegalArgumentException.class)
public void encodeNullRawPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
encoder.encode(null);
}
@Test(expected = IllegalArgumentException.class)
public void matchNullRawPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
encoder.matches(null, "does-not-matter");
}
} }
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
[[spring-security-crypto-encryption-bytes]] [[spring-security-crypto-encryption-bytes]]
==== BytesEncryptor ==== BytesEncryptor
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor: Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
[source,java] [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. This method requires Java 6.
The password used to generate the SecretKey should be kept in a secure place and not be shared. 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. 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 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]] [[spring-security-crypto-encryption-text]]
==== TextEncryptor ==== TextEncryptor
Use the Encryptors.text factory method to construct a standard TextEncryptor: Use the Encryptors.text factory method to construct a standard TextEncryptor:
@@ -41,7 +41,7 @@ A pre-invocation decision on whether the invocation is allowed to proceed is mad
[[authz-access-decision-manager]] [[authz-access-decision-manager]]
==== The AccessDecisionManager ==== The AccessDecisionManager
The `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` and is responsible for making final access control decisions. The `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` and is responsible for making final access control decisions.
the `AccessDecisionManager` interface contains three methods: The `AccessDecisionManager` interface contains three methods:
[source,java] [source,java]
---- ----
+3 -3
View File
@@ -1,3 +1,3 @@
gaeVersion=1.9.71 gaeVersion=1.9.80
springBootVersion=2.1.12.RELEASE springBootVersion=2.1.14.RELEASE
version=5.1.8.RELEASE version=5.1.11.RELEASE
+36 -36
View File
@@ -1,13 +1,13 @@
if (!project.hasProperty('reactorVersion')) { if (!project.hasProperty('reactorVersion')) {
ext.reactorVersion = 'Californium-SR15' ext.reactorVersion = 'Californium-SR18'
} }
if (!project.hasProperty('springVersion')) { if (!project.hasProperty('springVersion')) {
ext.springVersion = '5.1.13.RELEASE' ext.springVersion = '5.1.15.RELEASE'
} }
if (!project.hasProperty('springDataVersion')) { if (!project.hasProperty('springDataVersion')) {
ext.springDataVersion = 'Lovelace-SR15' ext.springDataVersion = 'Lovelace-SR17'
} }
dependencyManagement { dependencyManagement {
@@ -18,16 +18,16 @@ dependencyManagement {
} }
dependencies { dependencies {
dependency 'cglib:cglib-nodep:3.2.12' dependency 'cglib:cglib-nodep:3.2.12'
dependency 'com.squareup.okhttp3:mockwebserver:3.12.8' dependency 'com.squareup.okhttp3:mockwebserver:3.12.12'
dependency 'opensymphony:sitemesh:2.4.2' dependency 'opensymphony:sitemesh:2.4.2'
dependency 'org.gebish:geb-spock:0.10.0' dependency 'org.gebish:geb-spock:0.10.0'
dependency 'org.jasig.cas:cas-server-webapp:4.2.7' dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
dependency 'org.powermock:powermock-api-mockito2:2.0.5' dependency 'org.powermock:powermock-api-mockito2:2.0.7'
dependency 'org.powermock:powermock-api-support:2.0.5' dependency 'org.powermock:powermock-api-support:2.0.7'
dependency 'org.powermock:powermock-core:2.0.5' dependency 'org.powermock:powermock-core:2.0.7'
dependency 'org.powermock:powermock-module-junit4-common:2.0.5' dependency 'org.powermock:powermock-module-junit4-common:2.0.7'
dependency 'org.powermock:powermock-module-junit4:2.0.5' dependency 'org.powermock:powermock-module-junit4:2.0.7'
dependency 'org.powermock:powermock-reflect:2.0.5' dependency 'org.powermock:powermock-reflect:2.0.7'
dependency 'org.python:jython:2.5.3' dependency 'org.python:jython:2.5.3'
dependency 'org.spockframework:spock-core:1.0-groovy-2.4' dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4' dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
@@ -45,22 +45,22 @@ dependencyManagement {
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.10.2' dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.10.2'
dependency 'com.fasterxml:classmate:1.3.4' dependency 'com.fasterxml:classmate:1.3.4'
dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1' dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1'
dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.76' dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.80'
dependency 'com.google.appengine:appengine-api-labs:1.9.76' dependency 'com.google.appengine:appengine-api-labs:1.9.80'
dependency 'com.google.appengine:appengine-api-stubs:1.9.76' dependency 'com.google.appengine:appengine-api-stubs:1.9.80'
dependency 'com.google.appengine:appengine-testing:1.9.76' dependency 'com.google.appengine:appengine-testing:1.9.80'
dependency 'com.google.appengine:appengine:1.9.76' dependency 'com.google.appengine:appengine:1.9.80'
dependency 'com.google.code.gson:gson:2.8.2' dependency 'com.google.code.gson:gson:2.8.2'
dependency 'com.google.guava:guava:20.0' dependency 'com.google.guava:guava:20.0'
dependency 'com.google.inject:guice:3.0' dependency 'com.google.inject:guice:3.0'
dependency 'com.nimbusds:lang-tag:1.4.3' dependency 'com.nimbusds:lang-tag:1.4.3'
dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2' dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2'
dependency 'com.nimbusds:oauth2-oidc-sdk:6.0' dependency 'com.nimbusds:oauth2-oidc-sdk:6.0'
dependency 'com.squareup.okhttp3:okhttp:3.12.8' dependency 'com.squareup.okhttp3:okhttp:3.12.12'
dependency 'com.squareup.okio:okio:1.13.0' 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-core:2.3.0.1'
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2' dependency 'com.sun.xml.bind:jaxb-impl:2.3.3'
dependency 'com.unboundid:unboundid-ldapsdk:4.0.11' dependency 'com.unboundid:unboundid-ldapsdk:4.0.14'
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1' dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
dependency 'commons-cli:commons-cli:1.4' dependency 'commons-cli:commons-cli:1.4'
dependency 'commons-codec:commons-codec:1.11' dependency 'commons-codec:commons-codec:1.11'
@@ -127,7 +127,7 @@ dependencyManagement {
dependency 'org.apache.directory.shared:shared-cursor:0.9.15' 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-constants:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap:0.9.15' dependency 'org.apache.directory.shared:shared-ldap:0.9.15'
dependency 'org.apache.httpcomponents:httpclient:4.5.11' dependency 'org.apache.httpcomponents:httpclient:4.5.12'
dependency 'org.apache.httpcomponents:httpcore:4.4.8' dependency 'org.apache.httpcomponents:httpcore:4.4.8'
dependency 'org.apache.httpcomponents:httpmime:4.5.3' dependency 'org.apache.httpcomponents:httpmime:4.5.3'
dependency 'org.apache.mina:mina-core:2.0.0-M6' dependency 'org.apache.mina:mina-core:2.0.0-M6'
@@ -147,21 +147,21 @@ dependencyManagement {
dependency 'org.attoparser:attoparser:2.0.4.RELEASE' dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64' dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
dependency 'org.bouncycastle:bcprov-jdk15on:1.58' dependency 'org.bouncycastle:bcprov-jdk15on:1.58'
dependency 'org.codehaus.groovy:groovy-all:2.4.17' dependency 'org.codehaus.groovy:groovy-all:2.4.19'
dependency 'org.codehaus.groovy:groovy-json:2.4.17' dependency 'org.codehaus.groovy:groovy-json:2.4.19'
dependency 'org.codehaus.groovy:groovy:2.4.14' dependency 'org.codehaus.groovy:groovy:2.4.19'
dependency 'org.eclipse.jdt:ecj:3.12.3' 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-api:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.19.v20190610' dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610' dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
dependency 'org.eclipse.persistence:javax.persistence:2.2.1' dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
dependency 'org.gebish:geb-ast:0.10.0' dependency 'org.gebish:geb-ast:0.10.0'
dependency 'org.gebish:geb-core:0.10.0' dependency 'org.gebish:geb-core:0.10.0'
@@ -170,9 +170,9 @@ dependencyManagement {
dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hamcrest:hamcrest-core:1.3'
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final' 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.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
dependency 'org.hibernate:hibernate-core:5.2.17.Final' dependency 'org.hibernate:hibernate-core:5.2.18.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final' dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final'
dependency 'org.hibernate:hibernate-validator:6.0.18.Final' dependency 'org.hibernate:hibernate-validator:6.0.19.Final'
dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.hsqldb:hsqldb:2.4.1'
dependency 'org.jasig.cas.client:cas-client-core:3.5.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
dependency 'org.javassist:javassist:3.22.0-CR2' dependency 'org.javassist:javassist:3.22.0-CR2'
@@ -183,7 +183,7 @@ dependencyManagement {
dependency 'org.objenesis:objenesis:2.6' dependency 'org.objenesis:objenesis:2.6'
dependency 'org.openid4java:openid4java-nodeps:0.9.6' dependency 'org.openid4java:openid4java-nodeps:0.9.6'
dependency 'org.ow2.asm:asm:6.2.1' 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.33.3' dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.3'
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59' dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59' dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
@@ -16,6 +16,7 @@
package org.springframework.security.ldap.authentication.ad; package org.springframework.security.ldap.authentication.ad;
import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.DefaultDirObjectFactory; import org.springframework.ldap.core.support.DefaultDirObjectFactory;
@@ -24,6 +25,7 @@ import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@@ -142,12 +144,15 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
UsernamePasswordAuthenticationToken auth) { UsernamePasswordAuthenticationToken auth) {
String username = auth.getName(); String username = auth.getName();
String password = (String) auth.getCredentials(); String password = (String) auth.getCredentials();
DirContext ctx = null;
DirContext ctx = bindAsUser(username, password);
try { try {
ctx = bindAsUser(username, password);
return searchForUser(ctx, username); return searchForUser(ctx, username);
} }
catch (CommunicationException e) {
throw badLdapConnection(e);
}
catch (NamingException e) { catch (NamingException e) {
logger.error("Failed to locate directory entry for authenticated user: " logger.error("Failed to locate directory entry for authenticated user: "
+ username, e); + username, e);
@@ -210,8 +215,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|| (e instanceof OperationNotSupportedException)) { || (e instanceof OperationNotSupportedException)) {
handleBindException(bindPrincipal, e); handleBindException(bindPrincipal, e);
throw badCredentials(e); throw badCredentials(e);
} } else {
else {
throw LdapUtils.convertLdapException(e); throw LdapUtils.convertLdapException(e);
} }
} }
@@ -313,6 +317,12 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
return (BadCredentialsException) badCredentials().initCause(cause); return (BadCredentialsException) badCredentials().initCause(cause);
} }
private InternalAuthenticationServiceException badLdapConnection(Throwable cause) {
return new InternalAuthenticationServiceException(messages.getMessage(
"LdapAuthenticationProvider.badLdapConnection",
"Connection to LDAP server failed."), cause);
}
private DirContextOperations searchForUser(DirContext context, String username) private DirContextOperations searchForUser(DirContext context, String username)
throws NamingException { throws NamingException {
SearchControls searchControls = new SearchControls(); SearchControls searchControls = new SearchControls();
@@ -327,6 +337,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
searchControls, searchRoot, searchFilter, searchControls, searchRoot, searchFilter,
new Object[] { bindPrincipal, username }); new Object[] { bindPrincipal, username });
} }
catch (CommunicationException ldapCommunicationException) {
throw badLdapConnection(ldapCommunicationException);
}
catch (IncorrectResultSizeDataAccessException incorrectResults) { catch (IncorrectResultSizeDataAccessException incorrectResults) {
// Search should never return multiple results if properly configured - just // Search should never return multiple results if properly configured - just
// rethrow // rethrow
@@ -32,6 +32,7 @@ import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -58,6 +59,9 @@ import static org.springframework.security.ldap.authentication.ad.ActiveDirector
* @author Rob Winch * @author Rob Winch
*/ */
public class ActiveDirectoryLdapAuthenticationProviderTests { public class ActiveDirectoryLdapAuthenticationProviderTests {
public static final String EXISTING_LDAP_PROVIDER = "ldap://192.168.1.200/";
public static final String NON_EXISTING_LDAP_PROVIDER = "ldap://192.168.1.201/";
@Rule @Rule
public ExpectedException thrown = ExpectedException.none(); public ExpectedException thrown = ExpectedException.none();
@@ -378,17 +382,29 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
} }
@Test(expected = org.springframework.ldap.CommunicationException.class) @Test(expected = org.springframework.ldap.CommunicationException.class)
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Throwable {
throws Exception { try {
provider.contextFactory = createContextFactoryThrowing(new CommunicationException( provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
msg)); msg));
provider.authenticate(joe); provider.authenticate(joe);
} catch (InternalAuthenticationServiceException e) {
// Since GH-8418 ldap communication exception is wrapped into InternalAuthenticationServiceException.
// This test is about the wrapped exception, so we throw it.
throw e.getCause();
}
}
@Test(expected = org.springframework.security.authentication.InternalAuthenticationServiceException.class )
public void connectionExceptionIsWrappedInInternalException() throws Exception {
ActiveDirectoryLdapAuthenticationProvider noneReachableProvider = new ActiveDirectoryLdapAuthenticationProvider(
"mydomain.eu", NON_EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
noneReachableProvider.doAuthentication(joe);
} }
@Test @Test
public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception { public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider( ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
"mydomain.eu", "ldap://192.168.1.200/", "dc=ad,dc=eu,dc=mydomain"); "mydomain.eu", EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider); checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider);
} }
@@ -414,8 +430,11 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
provider.authenticate(joe); provider.authenticate(joe);
fail("CommunicationException was expected with a root cause of ClassNotFoundException"); fail("CommunicationException was expected with a root cause of ClassNotFoundException");
} }
catch (org.springframework.ldap.CommunicationException expected) { catch (InternalAuthenticationServiceException expected) {
assertThat(expected.getRootCause()).isInstanceOf(ClassNotFoundException.class); assertThat(expected.getCause()).isInstanceOf(org.springframework.ldap.CommunicationException.class);
org.springframework.ldap.CommunicationException cause =
(org.springframework.ldap.CommunicationException) expected.getCause();
assertThat(cause.getRootCause()).isInstanceOf(ClassNotFoundException.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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), authorizationCodeAuthentication.getAuthorizationExchange(),
accessTokenResponse.getAccessToken(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken()); accessTokenResponse.getRefreshToken(),
accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult; return authenticationResult;
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error; 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.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert; 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> * @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 { public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient; private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService; private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient, OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) { OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
Assert.notNull(userService, "userService cannot be null"); Assert.notNull(userService, "userService cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient; this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
this.userService = userService; this.userService = userService;
} }
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication = OAuth2LoginAuthenticationToken loginAuthenticationToken =
(OAuth2LoginAuthenticationToken) authentication; (OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope // scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (authorizationCodeAuthentication.getAuthorizationExchange() if (loginAuthenticationToken.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) { .getAuthorizationRequest().getScopes().contains("openid")) {
// This is an OpenID Connect Authentication Request so return null // This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead // and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null; return null;
} }
OAuth2AccessTokenResponse accessTokenResponse; OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try { try {
OAuth2AuthorizationExchangeValidator.validate( authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
authorizationCodeAuthentication.getAuthorizationExchange()); .authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( loginAuthenticationToken.getAuthorizationExchange()));
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) { } catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError(); OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} }
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters(); Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters)); loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(), loginAuthenticationToken.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, oauth2User,
mappedAuthorities, mappedAuthorities,
accessToken, accessToken,
accessTokenResponse.getRefreshToken()); authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult; return authenticationResult;
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.MultiValueMap;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; 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, * 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) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
if (this.shouldProcessAuthorizationResponse(request)) { if (matchesAuthorizationResponse(request)) {
this.processAuthorizationResponse(request, response); processAuthorizationResponse(request, response);
return; return;
} }
filterChain.doFilter(request, response); 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); OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest == null) { if (authorizationRequest == null) {
return false; return false;
} }
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
request.getServerPort(), request.getRequestURI(), null); // Compare redirect_uri
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
if (requestUrl.equals(authorizationRequest.getRedirectUri()) && UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build();
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { 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 true;
} }
return false; return false;
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) String redirectUri = UrlUtils.buildFullRequestUrl(request);
.replaceQuery(null)
.build()
.toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri); OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken( OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
} catch (OAuth2AuthorizationException ex) { } catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError(); OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authorizationResponse.getRedirectUri()) .fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode()); .queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.isEmpty(error.getDescription())) { if (!StringUtils.isEmpty(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, 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); this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
String redirectUrl = authorizationResponse.getRedirectUri(); String redirectUrl = authorizationRequest.getRedirectUri();
SavedRequest savedRequest = this.requestCache.getRequest(request, response); SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) { if (savedRequest != null) {
redirectUrl = savedRequest.getRedirectUrl(); redirectUrl = savedRequest.getRedirectUrl();
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono; 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, * A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
* which handles the processing of the OAuth 2.0 Authorization Response. * which handles the processing of the OAuth 2.0 Authorization Response.
@@ -138,10 +145,10 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.requiresAuthenticationMatcher.matches(exchange) return this.requiresAuthenticationMatcher.matches(exchange)
.filter( matchResult -> matchResult.isMatch()) .filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap( matchResult -> this.authenticationConverter.convert(exchange)) .flatMap(matchResult -> this.authenticationConverter.convert(exchange))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty())) .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, private Mono<Void> authenticate(ServerWebExchange exchange,
@@ -171,20 +178,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
} }
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) { private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange) return Mono.just(exchange)
.flatMap(authorizationRequest -> { .filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) .flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
.query(null) .flatMap(authorizationRequest ->
.build() matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
.toUriString();
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(queryParams)) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
})
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); .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-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -103,14 +102,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
} }
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) { private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
MultiValueMap<String, String> queryParams = exchange.getRequest()
.getQueryParams();
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.query(null)
.build() .build()
.toUriString(); .toUriString();
return OAuth2AuthorizationResponseUtils return OAuth2AuthorizationResponseUtils
.convert(queryParams, redirectUri); .convert(exchange.getRequest().getQueryParams(), redirectUri);
} }
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,6 +15,10 @@
*/ */
package org.springframework.security.oauth2.client.authentication; 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.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -33,13 +37,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse;
/** /**
* Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}. * Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}.
@@ -130,4 +133,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken); assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken);
assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken); assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken);
} }
// 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,
this.authorizationResponse);
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-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -18,10 +18,6 @@ package org.springframework.security.oauth2.client.web;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -39,36 +35,44 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache; 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.FilterChain;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*; 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;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges.success;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
/** /**
* Tests for {@link OAuth2AuthorizationCodeGrantFilter}. * Tests for {@link OAuth2AuthorizationCodeGrantFilter}.
* *
* @author Joe Grandja * @author Joe Grandja
*/ */
@PowerMockIgnore("javax.security.*")
@PrepareForTest({OAuth2AuthorizationRequest.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationCodeGrantFilter.class})
@RunWith(PowerMockRunner.class)
public class OAuth2AuthorizationCodeGrantFilterTests { public class OAuth2AuthorizationCodeGrantFilterTests {
private ClientRegistration registration1; private ClientRegistration registration1;
private String principalName1 = "principal-1"; private String principalName1 = "principal-1";
@@ -132,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri); request.setServletPath(requestUri);
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter. // NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
MockHttpServletResponse response = new MockHttpServletResponse();
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(request, response, filterChain);
@@ -143,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
@Test @Test
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception { public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
String requestUri = "/path"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
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");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); 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.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 @Test
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception { public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception {
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT); OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
when(this.authenticationManager.authenticate(any(Authentication.class))) when(this.authenticationManager.authenticate(any(Authentication.class)))
.thenThrow(new OAuth2AuthorizationException(error)); .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"); assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception { public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
this.registration1.getRegistrationId(), this.principalName1); this.registration1.getRegistrationId(), this.principalName1);
@@ -242,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception { public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(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"); assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception { public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
String requestUri = "/saved-request"; String requestUri = "/saved-request";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri); request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
RequestCache requestCache = new HttpSessionRequestCache(); RequestCache requestCache = new HttpSessionRequestCache();
requestCache.saveRequest(request, response); requestCache.saveRequest(request, response);
request.setRequestURI("/callback/client-1");
requestUri = "/callback/client-1";
request.setRequestURI(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code"); request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state"); request.addParameter(OAuth2ParameterNames.STATE, "state");
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1); this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
@@ -285,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception { public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
AnonymousAuthenticationToken anonymousPrincipal = AnonymousAuthenticationToken anonymousPrincipal =
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(anonymousPrincipal); securityContext.setAuthentication(anonymousPrincipal);
SecurityContextHolder.setContext(securityContext); SecurityContextHolder.setContext(securityContext);
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), anonymousPrincipal, request); this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
assertThat(authorizedClient).isNotNull(); assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName()); assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
assertThat(authorizedClient.getAccessToken()).isNotNull(); assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false); HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull(); assertThat(session).isNotNull();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -326,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception { public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(securityContext); // null Authentication SecurityContextHolder.setContext(securityContext); // null Authentication
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), null, request); this.registration1.getRegistrationId(), null, authorizationResponse);
assertThat(authorizedClient).isNotNull(); assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser"); assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
assertThat(authorizedClient.getAccessToken()).isNotNull(); assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false); HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull(); assertThat(session).isNotNull();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -363,23 +393,57 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); 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, private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
ClientRegistration registration) { ClientRegistration registration) {
Map<String, Object> additionalParameters = new HashMap<>(); Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId()); additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class); OAuth2AuthorizationRequest authorizationRequest = request()
when(authorizationRequest.getAdditionalParameters()).thenReturn(additionalParameters); .additionalParameters(additionalParameters)
when(authorizationRequest.getRedirectUri()).thenReturn(request.getRequestURL().toString()); .redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
when(authorizationRequest.getState()).thenReturn("state");
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
} }
private void setUpAuthenticationResult(ClientRegistration registration) { private void setUpAuthenticationResult(ClientRegistration registration) {
OAuth2AuthorizationCodeAuthenticationToken authentication = mock(OAuth2AuthorizationCodeAuthenticationToken.class); OAuth2AuthorizationCodeAuthenticationToken authentication =
when(authentication.getClientRegistration()).thenReturn(registration); new OAuth2AuthorizationCodeAuthenticationToken(registration, success(), noScopes(), refreshToken());
when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class));
when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
when(authentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class));
when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
} }
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.ReactiveAuthenticationManager; 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.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; 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.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; import org.springframework.util.CollectionUtils;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.handler.DefaultWebFilterChain; import org.springframework.web.server.handler.DefaultWebFilterChain;
import reactor.core.publisher.Mono; 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.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any; 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 * @author Rob Winch
@@ -101,7 +104,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
MockServerWebExchange exchange = MockServerWebExchange MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/")); .from(MockServerHttpRequest.get("/"));
DefaultWebFilterChain chain = new DefaultWebFilterChain( DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete()); e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block(); this.filter.filter(exchange, chain).block();
@@ -110,42 +113,154 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
@Test @Test
public void filterWhenMatchThenAuthorizedClientSaved() { public void filterWhenMatchThenAuthorizedClientSaved() {
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
.redirectUri("/authorize/registration-id") when(this.clientRegistrationRepository.findByRegistrationId(any()))
.build(); .thenReturn(Mono.just(clientRegistration));
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();
when(this.authenticationManager.authenticate(any())).thenReturn(
Mono.just(authenticated));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())) when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
ServerAuthenticationConverter converter = e -> authentication; when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
this.filter = new OAuth2AuthorizationCodeGrantWebFilter( MockServerHttpRequest authorizationRequest =
this.authenticationManager, converter, this.authorizedClientRepository); createAuthorizationRequest("/authorization/callback");
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
MockServerHttpRequest request = MockServerHttpRequest createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
.get("/authorize/registration-id") MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
.queryParam(OAuth2ParameterNames.CODE, "code") MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
.queryParam(OAuth2ParameterNames.STATE, "state") this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
DefaultWebFilterChain chain = new DefaultWebFilterChain( DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete()); e -> e.getResponse().setComplete(), Collections.emptyList());
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block(); this.filter.filter(exchange, chain).block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any()); 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);
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
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.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
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);
// 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);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
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.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
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.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
}
private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(
MockServerHttpRequest authorizationRequest, ClientRegistration registration) {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
return request()
.additionalParameters(additionalParameters)
.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-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -57,8 +57,8 @@ import java.util.stream.Stream;
public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> { public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE = private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {}; new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -82,10 +82,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
try { try {
// gh-6463
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked") @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); 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) { } catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " + throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
ex.getMessage(), ex, inputMessage); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
/** /**
* A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}. * A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}.
@@ -47,8 +48,8 @@ import java.util.Map;
public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> { public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE = private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {}; new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -70,10 +71,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
try { try {
// gh-8157
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked") @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); 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) { } catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " + throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
ex.getMessage(), ex, inputMessage); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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() throws Exception {
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 @Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter tokenResponseConverter = mock(Converter.class); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); 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 @Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter errorConverter = mock(Converter.class); Converter errorConverter = mock(Converter.class);
@@ -66,7 +66,7 @@ class JettyCasService extends Server {
String password = System.getProperty('javax.net.ssl.trustStorePassword','password') String password = System.getProperty('javax.net.ssl.trustStorePassword','password')
SslContextFactory sslContextFactory = new SslContextFactory(); SslContextFactory sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(getTrustStore()); sslContextFactory.setKeyStorePath(getTrustStore());
sslContextFactory.setKeyStorePassword(password); sslContextFactory.setKeyStorePassword(password);
sslContextFactory.setKeyManagerPassword(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(); .username() : withUser.value();
if (username == null) { if (username == null) {
throw new IllegalArgumentException(withUser 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<>(); List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
@@ -60,7 +60,7 @@ import java.util.*;
* requests which match the pattern. An example configuration might look like this: * requests which match the pattern. An example configuration might look like this:
* *
* <pre> * <pre>
* &lt;bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy"&gt; * &lt;bean id="myfilterChainProxy" class="org.springframework.security.web.FilterChainProxy"&gt;
* &lt;constructor-arg&gt; * &lt;constructor-arg&gt;
* &lt;util:list&gt; * &lt;util:list&gt;
* &lt;security:filter-chain pattern="/do/not/filter*" filters="none"/&gt; * &lt;security:filter-chain pattern="/do/not/filter*" filters="none"/&gt;
@@ -228,10 +228,15 @@ class DummyRequest extends HttpServletRequestWrapper {
public void setQueryString(String queryString) { public void setQueryString(String queryString) {
this.queryString = queryString; this.queryString = queryString;
} }
@Override
public String getServerName() {
return null;
}
} }
final class UnsupportedOperationExceptionInvocationHandler implements InvocationHandler { final class UnsupportedOperationExceptionInvocationHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
throw new UnsupportedOperationException(method + " is not supported"); throw new UnsupportedOperationException(method + " is not supported");
} }
} }
@@ -563,6 +563,6 @@ public class SwitchUserFilter extends GenericFilterBean
} }
private static RequestMatcher createMatcher(String pattern) { private static RequestMatcher createMatcher(String pattern) {
return new AntPathRequestMatcher(pattern, null, true, new UrlPathHelper()); return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
} }
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -16,16 +16,17 @@
package org.springframework.security.web.firewall; package org.springframework.security.web.firewall;
import org.springframework.http.HttpMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
/** /**
* <p> * <p>
@@ -66,10 +67,15 @@ import java.util.Set;
* Rejects URLs that contain a URL encoded percent. See * Rejects URLs that contain a URL encoded percent. See
* {@link #setAllowUrlEncodedPercent(boolean)} * {@link #setAllowUrlEncodedPercent(boolean)}
* </li> * </li>
* <li>
* Rejects hosts that are not allowed. See
* {@link #setAllowedHostnames(Predicate)}
* </li>
* </ul> * </ul>
* *
* @see DefaultHttpFirewall * @see DefaultHttpFirewall
* @author Rob Winch * @author Rob Winch
* @author Eddú Meléndez
* @since 4.2.4 * @since 4.2.4
*/ */
public class StrictHttpFirewall implements HttpFirewall { public class StrictHttpFirewall implements HttpFirewall {
@@ -96,6 +102,8 @@ public class StrictHttpFirewall implements HttpFirewall {
private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods(); private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();
private Predicate<String> allowedHostnames = hostname -> true;
public StrictHttpFirewall() { public StrictHttpFirewall() {
urlBlacklistsAddAll(FORBIDDEN_SEMICOLON); urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH); urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
@@ -277,6 +285,21 @@ public class StrictHttpFirewall implements HttpFirewall {
} }
} }
/**
* <p>
* Determines which hostnames should be allowed. The default is to allow any hostname.
* </p>
*
* @param allowedHostnames the predicate for testing hostnames
* @since 5.1.11
*/
public void setAllowedHostnames(Predicate<String> allowedHostnames) {
if (allowedHostnames == null) {
throw new IllegalArgumentException("allowedHostnames cannot be null");
}
this.allowedHostnames = allowedHostnames;
}
private void urlBlacklistsAddAll(Collection<String> values) { private void urlBlacklistsAddAll(Collection<String> values) {
this.encodedUrlBlacklist.addAll(values); this.encodedUrlBlacklist.addAll(values);
this.decodedUrlBlacklist.addAll(values); this.decodedUrlBlacklist.addAll(values);
@@ -291,6 +314,7 @@ public class StrictHttpFirewall implements HttpFirewall {
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
rejectForbiddenHttpMethod(request); rejectForbiddenHttpMethod(request);
rejectedBlacklistedUrls(request); rejectedBlacklistedUrls(request);
rejectedUntrustedHosts(request);
if (!isNormalized(request)) { if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized."); throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
@@ -332,6 +356,13 @@ public class StrictHttpFirewall implements HttpFirewall {
} }
} }
private void rejectedUntrustedHosts(HttpServletRequest request) {
String serverName = request.getServerName();
if (serverName != null && !this.allowedHostnames.test(serverName)) {
throw new RequestRejectedException("The request was rejected because the domain " + serverName + " is untrusted.");
}
}
@Override @Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) { public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new FirewalledResponse(response); return new FirewalledResponse(response);
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -55,6 +55,7 @@ import org.springframework.web.server.WebFilterChain;
* </p> * </p>
* *
* @author Rob Winch * @author Rob Winch
* @author Parikshit Dutta
* @since 5.0 * @since 5.0
*/ */
public class CsrfWebFilter implements WebFilter { public class CsrfWebFilter implements WebFilter {
@@ -136,7 +137,7 @@ public class CsrfWebFilter implements WebFilter {
@Override @Override
public Mono<MatchResult> matches(ServerWebExchange exchange) { public Mono<MatchResult> matches(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest()) return Mono.just(exchange.getRequest())
.map(r -> r.getMethod()) .flatMap(r -> Mono.justOrEmpty(r.getMethod()))
.filter(m -> ALLOWED_METHODS.contains(m)) .filter(m -> ALLOWED_METHODS.contains(m))
.flatMap(m -> MatchResult.notMatch()) .flatMap(m -> MatchResult.notMatch())
.switchIfEmpty(MatchResult.match()); .switchIfEmpty(MatchResult.match());
@@ -42,7 +42,6 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint; 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.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@@ -82,7 +81,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
private LogoutHandler logoutHandler; private List<LogoutHandler> logoutHandlers;
HttpServlet3RequestFactory(String rolePrefix) { HttpServlet3RequestFactory(String rolePrefix) {
this.rolePrefix = rolePrefix; this.rolePrefix = rolePrefix;
@@ -146,7 +145,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
* {@link HttpServletRequest#logout()}. * {@link HttpServletRequest#logout()}.
*/ */
public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) { public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) {
this.logoutHandler = CollectionUtils.isEmpty(logoutHandlers) ? null : new CompositeLogoutHandler(logoutHandlers); this.logoutHandlers = logoutHandlers;
} }
/** /**
@@ -246,8 +245,8 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
@Override @Override
public void logout() throws ServletException { public void logout() throws ServletException {
LogoutHandler handler = HttpServlet3RequestFactory.this.logoutHandler; List<LogoutHandler> handlers = HttpServlet3RequestFactory.this.logoutHandlers;
if (handler == null) { if (CollectionUtils.isEmpty(handlers)) {
HttpServlet3RequestFactory.this.logger.debug( HttpServlet3RequestFactory.this.logger.debug(
"logoutHandlers is null, so allowing original HttpServletRequest to handle logout"); "logoutHandlers is null, so allowing original HttpServletRequest to handle logout");
super.logout(); super.logout();
@@ -255,7 +254,9 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
} }
Authentication authentication = SecurityContextHolder.getContext() Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication(); .getAuthentication();
handler.logout(this, this.response, authentication); for (LogoutHandler handler : handlers) {
handler.logout(this, this.response, authentication);
}
} }
private boolean isAuthenticated() { private boolean isAuthenticated() {
@@ -67,7 +67,7 @@ public final class AntPathRequestMatcher
/** /**
* Creates a matcher with the specific pattern which will match all HTTP methods in a * Creates a matcher with the specific pattern which will match all HTTP methods in a
* case insensitive manner. * case sensitive manner.
* *
* @param pattern the ant pattern to use for matching * @param pattern the ant pattern to use for matching
*/ */
@@ -76,7 +76,7 @@ public final class AntPathRequestMatcher
} }
/** /**
* Creates a matcher with the supplied pattern and HTTP method in a case insensitive * Creates a matcher with the supplied pattern and HTTP method in a case sensitive
* manner. * manner.
* *
* @param pattern the ant pattern to use for matching * @param pattern the ant pattern to use for matching
@@ -107,7 +107,7 @@ public final class AntPathRequestMatcher
* *
* @param pattern the ant pattern to use for matching * @param pattern the ant pattern to use for matching
* @param httpMethod the HTTP method. The {@code matches} method will return false if * @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 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 * @param urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest
*/ */
@@ -16,11 +16,17 @@
package org.springframework.security.web.authentication.switchuser; package org.springframework.security.web.authentication.switchuser;
import static org.assertj.core.api.Assertions.*; import java.util.ArrayList;
import static org.mockito.Mockito.*; import java.util.Collection;
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.junit.rules.ExpectedException;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.AccountExpiredException;
@@ -42,8 +48,10 @@ import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import javax.servlet.FilterChain; import static org.assertj.core.api.Assertions.assertThat;
import java.util.*; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/** /**
* Tests * Tests
@@ -75,6 +83,7 @@ public class SwitchUserFilterTests {
request.setScheme("http"); request.setScheme("http");
request.setServerName("localhost"); request.setServerName("localhost");
request.setRequestURI("/login/impersonate"); request.setRequestURI("/login/impersonate");
request.setMethod("POST");
return request; return request;
} }
@@ -125,6 +134,20 @@ public class SwitchUserFilterTests {
assertThat(filter.requiresExitUser(request)).isFalse(); 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 @Test
public void requiresExitUserWhenMatcherThenWorks() { public void requiresExitUserWhenMatcherThenWorks() {
SwitchUserFilter filter = new SwitchUserFilter(); SwitchUserFilter filter = new SwitchUserFilter();
@@ -159,6 +182,20 @@ public class SwitchUserFilterTests {
assertThat(filter.requiresSwitchUser(request)).isFalse(); 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 @Test
public void requiresSwitchUserWhenMatcherThenWorks() { public void requiresSwitchUserWhenMatcherThenWorks() {
SwitchUserFilter filter = new SwitchUserFilter(); SwitchUserFilter filter = new SwitchUserFilter();
@@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
/** /**
* @author Rob Winch * @author Rob Winch
* @author Eddú Meléndez
*/ */
public class StrictHttpFirewallTests { public class StrictHttpFirewallTests {
public String[] unnormalizedPaths = { "/..", "/./path/", "/path/path/.", "/path/path//.", "./path/../path//.", public String[] unnormalizedPaths = { "/..", "/./path/", "/path/path/.", "/path/path//.", "./path/../path//.",
@@ -428,4 +429,20 @@ public class StrictHttpFirewallTests {
this.firewall.getFirewalledRequest(request); this.firewall.getFirewalledRequest(request);
} }
@Test
public void getFirewalledRequestWhenTrustedDomainThenNoException() {
this.request.addHeader("Host", "example.org");
this.firewall.setAllowedHostnames(hostname -> hostname.equals("example.org"));
assertThatCode(() -> this.firewall.getFirewalledRequest(this.request)).doesNotThrowAnyException();
}
@Test(expected = RequestRejectedException.class)
public void getFirewalledRequestWhenUntrustedDomainThenException() {
this.request.addHeader("Host", "example.org");
this.firewall.setAllowedHostnames(hostname -> hostname.equals("myexample.org"));
this.firewall.getFirewalledRequest(this.request);
}
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -20,10 +20,14 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession; import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -33,9 +37,11 @@ import reactor.test.publisher.PublisherProbe;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.mock.web.server.MockServerWebExchange.from;
/** /**
* @author Rob Winch * @author Rob Winch
* @author Parikshit Dutta
* @since 5.0 * @since 5.0
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@@ -49,10 +55,10 @@ public class CsrfWebFilterTests {
private CsrfWebFilter csrfFilter = new CsrfWebFilter(); private CsrfWebFilter csrfFilter = new CsrfWebFilter();
private MockServerWebExchange get = MockServerWebExchange.from( private MockServerWebExchange get = from(
MockServerHttpRequest.get("/")); MockServerHttpRequest.get("/"));
private MockServerWebExchange post = MockServerWebExchange.from( private MockServerWebExchange post = from(
MockServerHttpRequest.post("/")); MockServerHttpRequest.post("/"));
@Test @Test
@@ -104,7 +110,7 @@ public class CsrfWebFilterTests {
this.csrfFilter.setCsrfTokenRepository(this.repository); this.csrfFilter.setCsrfTokenRepository(this.repository);
when(this.repository.loadToken(any())) when(this.repository.loadToken(any()))
.thenReturn(Mono.just(this.token)); .thenReturn(Mono.just(this.token));
this.post = MockServerWebExchange.from(MockServerHttpRequest.post("/") this.post = from(MockServerHttpRequest.post("/")
.body(this.token.getParameterName() + "="+this.token.getToken()+"INVALID")); .body(this.token.getParameterName() + "="+this.token.getToken()+"INVALID"));
Mono<Void> result = this.csrfFilter.filter(this.post, this.chain); Mono<Void> result = this.csrfFilter.filter(this.post, this.chain);
@@ -125,7 +131,7 @@ public class CsrfWebFilterTests {
.thenReturn(Mono.just(this.token)); .thenReturn(Mono.just(this.token));
when(this.repository.generateToken(any())) when(this.repository.generateToken(any()))
.thenReturn(Mono.just(this.token)); .thenReturn(Mono.just(this.token));
this.post = MockServerWebExchange.from(MockServerHttpRequest.post("/") this.post = from(MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED) .contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(this.token.getParameterName() + "="+this.token.getToken())); .body(this.token.getParameterName() + "="+this.token.getToken()));
@@ -142,7 +148,7 @@ public class CsrfWebFilterTests {
this.csrfFilter.setCsrfTokenRepository(this.repository); this.csrfFilter.setCsrfTokenRepository(this.repository);
when(this.repository.loadToken(any())) when(this.repository.loadToken(any()))
.thenReturn(Mono.just(this.token)); .thenReturn(Mono.just(this.token));
this.post = MockServerWebExchange.from(MockServerHttpRequest.post("/") this.post = from(MockServerHttpRequest.post("/")
.header(this.token.getHeaderName(), this.token.getToken()+"INVALID")); .header(this.token.getHeaderName(), this.token.getToken()+"INVALID"));
Mono<Void> result = this.csrfFilter.filter(this.post, this.chain); Mono<Void> result = this.csrfFilter.filter(this.post, this.chain);
@@ -163,7 +169,7 @@ public class CsrfWebFilterTests {
.thenReturn(Mono.just(this.token)); .thenReturn(Mono.just(this.token));
when(this.repository.generateToken(any())) when(this.repository.generateToken(any()))
.thenReturn(Mono.just(this.token)); .thenReturn(Mono.just(this.token));
this.post = MockServerWebExchange.from(MockServerHttpRequest.post("/") this.post = from(MockServerHttpRequest.post("/")
.header(this.token.getHeaderName(), this.token.getToken())); .header(this.token.getHeaderName(), this.token.getToken()));
Mono<Void> result = this.csrfFilter.filter(this.post, this.chain); Mono<Void> result = this.csrfFilter.filter(this.post, this.chain);
@@ -173,4 +179,14 @@ public class CsrfWebFilterTests {
chainResult.assertWasSubscribed(); chainResult.assertWasSubscribed();
} }
@Test
// gh-8452
public void matchesRequireCsrfProtectionWhenNonStandardHTTPMethodIsUsed() {
HttpMethod customHttpMethod = HttpMethod.resolve("non-standard-http-method");
MockServerWebExchange nonStandardHttpRequest = from(MockServerHttpRequest.method(customHttpMethod, "/"));
ServerWebExchangeMatcher serverWebExchangeMatcher = CsrfWebFilter.DEFAULT_CSRF_MATCHER;
assertThat(serverWebExchangeMatcher.matches(nonStandardHttpRequest).map(MatchResult::isMatch).block()).isTrue();
}
} }