Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37568780a1 | |||
| d80b1865a5 | |||
| 52c80c78e5 | |||
| ded83cc1b3 | |||
| 7a1833c1df | |||
| e6630ea0f1 | |||
| 2400e8fde2 | |||
| f05d70a4a5 | |||
| 8bb4e72aff | |||
| f58a262eb4 | |||
| c0154f2315 | |||
| cea2b556d6 | |||
| faa02e8bc0 | |||
| d9f57492d4 | |||
| b007fdc333 | |||
| 16c350a7bc | |||
| 6d6a22eda1 | |||
| 1e957e96b9 | |||
| 9986a33177 | |||
| ecf58c9549 | |||
| 497ee84537 | |||
| 30a68b6c10 | |||
| aa118466e5 | |||
| f98db2d866 | |||
| 52af3998c8 | |||
| 1b91259412 | |||
| 9d3ea58bb1 | |||
| c2296b0376 | |||
| 24d251f232 | |||
| 18610eab6f | |||
| 1964cfdfaa | |||
| 0ae7ceef70 | |||
| b3c7f0a79f | |||
| 4df4f9a63d | |||
| 9d25c2d2f8 | |||
| 94b116c8c8 | |||
| 3c6f08ede0 | |||
| 9a94234dae | |||
| 017044bf93 | |||
| ae99097723 | |||
| acb7e63cf7 | |||
| 0256439a50 | |||
| c8f72a1b87 | |||
| 332e8ce07a | |||
| 7aea459535 | |||
| c91389ff8b | |||
| 6de345b972 | |||
| 19f08cbedb | |||
| f82e435aaf | |||
| a44225d334 | |||
| 5decfb1ece | |||
| a24d67375b | |||
| 3a46ba8a85 | |||
| a4851095df | |||
| 633e5b85b4 | |||
| bfa5830e3d | |||
| e43fab518c | |||
| c9f676739b | |||
| b7ce65b284 | |||
| 67d561b5f7 | |||
| 4dd2b1dfe1 | |||
| aa28a0b453 | |||
| 8145cb557b | |||
| ef4109358a | |||
| 6d6fd09665 | |||
| 84f5713d26 | |||
| 137c211667 | |||
| 4e4785c6c2 | |||
| ec0e03cff2 | |||
| e3a50c5039 | |||
| b2476915e2 | |||
| 90b1c877f1 | |||
| 57e69156c5 | |||
| 4c3ac0a1a4 | |||
| d9521cb880 | |||
| 7d7cdbb09c | |||
| c2129d3812 | |||
| 097c4bf2a1 | |||
| b8938c98d3 | |||
| 0520810a0d | |||
| 8711ba5a30 | |||
| 0f8927b500 | |||
| 1136660518 | |||
| c395da3e04 | |||
| 798c48eee3 | |||
| b7f1bdae64 | |||
| 459e8f1a11 | |||
| 450a20add4 | |||
| f3f84e1aff | |||
| 4a9d13dc5c | |||
| 62529278b5 | |||
| dcb3b6940f | |||
| a82047e61b | |||
| 6c54e5f278 | |||
| 8589580aa2 | |||
| 1749c8df9c | |||
| a5391b629e | |||
| 57f3c76801 | |||
| f8f1e9a3d4 | |||
| bf78e43403 | |||
| 3599ad7547 | |||
| 143e3ac249 | |||
| 362356dea5 | |||
| f28fe2d501 | |||
| 5114190cb6 | |||
| 93d1c7f59f | |||
| 2c128e85fa | |||
| a17b75e862 | |||
| 178a5e0819 | |||
| 7542d59364 | |||
| 83b7d7d461 | |||
| 17b48b0b60 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
language: java
|
language: java
|
||||||
|
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- openjdk8
|
||||||
|
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
|
|||||||
+3
-4
@@ -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
|
||||||
// ==================================================================================================
|
// ==================================================================================================
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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.
|
||||||
|
|||||||
+4
@@ -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);
|
||||||
|
|||||||
+3
@@ -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
|
||||||
|
|||||||
+4
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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}.
|
||||||
|
|||||||
+19
-19
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+56
-24
@@ -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.
|
||||||
@@ -53,7 +53,6 @@ import org.springframework.security.authorization.AuthorityReactiveAuthorization
|
|||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
|
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
|
||||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager;
|
||||||
@@ -89,7 +88,6 @@ import org.springframework.security.web.server.DelegatingServerAuthenticationEnt
|
|||||||
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
|
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
|
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
|
||||||
@@ -619,32 +617,60 @@ public class ServerHttpSecurity {
|
|||||||
AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
|
AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
|
||||||
authenticationFilter.setRequiresAuthenticationMatcher(createAttemptAuthenticationRequestMatcher());
|
authenticationFilter.setRequiresAuthenticationMatcher(createAttemptAuthenticationRequestMatcher());
|
||||||
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
|
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
|
||||||
RedirectServerAuthenticationSuccessHandler redirectHandler = new RedirectServerAuthenticationSuccessHandler();
|
authenticationFilter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler());
|
||||||
|
authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error"));
|
||||||
authenticationFilter.setAuthenticationSuccessHandler(redirectHandler);
|
|
||||||
authenticationFilter.setAuthenticationFailureHandler(new ServerAuthenticationFailureHandler() {
|
|
||||||
@Override
|
|
||||||
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,
|
|
||||||
AuthenticationException exception) {
|
|
||||||
return Mono.error(exception);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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}");
|
||||||
}
|
}
|
||||||
@@ -1477,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) {
|
||||||
@@ -2386,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}
|
||||||
@@ -2450,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();
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-1
@@ -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 {
|
||||||
|
|||||||
+42
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-8
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -34,11 +34,17 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
|||||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||||
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.client.web.server.ServerAuthorizationRequestRepository;
|
||||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||||
|
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
|
||||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
@@ -69,8 +75,11 @@ public class OAuth2ClientSpecTests {
|
|||||||
|
|
||||||
private ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
|
private ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
|
||||||
|
|
||||||
|
private ApplicationContext context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setApplicationContext(ApplicationContext context) {
|
public void setApplicationContext(ApplicationContext context) {
|
||||||
|
this.context = context;
|
||||||
this.client = WebTestClient.bindToApplicationContext(context).build();
|
this.client = WebTestClient.bindToApplicationContext(context).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,19 +149,40 @@ public class OAuth2ClientSpecTests {
|
|||||||
|
|
||||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||||
ReactiveAuthenticationManager manager = config.manager;
|
ReactiveAuthenticationManager manager = config.manager;
|
||||||
|
ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
|
||||||
|
new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||||
|
|
||||||
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
|
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||||
|
.redirectUri("/authorize/oauth2/code/registration-id")
|
||||||
|
.build();
|
||||||
|
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
|
||||||
|
.redirectUri("/authorize/oauth2/code/registration-id")
|
||||||
|
.build();
|
||||||
|
OAuth2AuthorizationExchange authorizationExchange =
|
||||||
|
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
|
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
|
||||||
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken result = new OAuth2AuthorizationCodeAuthenticationToken(this.registration, exchange, accessToken);
|
OAuth2AuthorizationCodeAuthenticationToken result = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||||
|
this.registration, authorizationExchange, accessToken);
|
||||||
|
|
||||||
when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
|
when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
|
||||||
when(manager.authenticate(any())).thenReturn(Mono.just(result));
|
when(manager.authenticate(any())).thenReturn(Mono.just(result));
|
||||||
|
|
||||||
this.client.get()
|
WebTestClient client = WebTestClient.bindToApplicationContext(this.context)
|
||||||
.uri("/authorize/oauth2/code/registration-id")
|
.webFilter((exchange, chain) ->
|
||||||
.exchange()
|
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange)
|
||||||
.expectStatus().is3xxRedirection();
|
.then(chain.filter(exchange).then(Mono.empty()))
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
client.get()
|
||||||
|
.uri(uriBuilder ->
|
||||||
|
uriBuilder.path("/authorize/oauth2/code/registration-id")
|
||||||
|
.queryParam(OAuth2ParameterNames.CODE, "code")
|
||||||
|
.queryParam(OAuth2ParameterNames.STATE, "state")
|
||||||
|
.build())
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().is3xxRedirection();
|
||||||
|
|
||||||
verify(converter).convert(any());
|
verify(converter).convert(any());
|
||||||
verify(manager).authenticate(any());
|
verify(manager).authenticate(any());
|
||||||
|
|||||||
+125
-6
@@ -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.
|
||||||
@@ -26,21 +26,36 @@ 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;
|
||||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||||
import org.springframework.security.config.test.SpringTestRule;
|
import org.springframework.security.config.test.SpringTestRule;
|
||||||
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
||||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||||
@@ -48,12 +63,17 @@ 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.Instant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
@@ -63,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;
|
||||||
|
|
||||||
@@ -72,6 +94,20 @@ public class OAuth2LoginTests {
|
|||||||
.clientSecret("secret")
|
.clientSecret("secret")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
private static ClientRegistration google = CommonOAuth2Provider.GOOGLE
|
||||||
|
.getBuilder("google")
|
||||||
|
.clientId("client")
|
||||||
|
.clientSecret("secret")
|
||||||
|
.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();
|
||||||
@@ -97,11 +133,6 @@ public class OAuth2LoginTests {
|
|||||||
static class OAuth2LoginWithMulitpleClientRegistrations {
|
static class OAuth2LoginWithMulitpleClientRegistrations {
|
||||||
@Bean
|
@Bean
|
||||||
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||||
ClientRegistration google = CommonOAuth2Provider.GOOGLE
|
|
||||||
.getBuilder("google")
|
|
||||||
.clientId("client")
|
|
||||||
.clientSecret("secret")
|
|
||||||
.build();
|
|
||||||
return new InMemoryReactiveClientRegistrationRepository(github, google);
|
return new InMemoryReactiveClientRegistrationRepository(github, google);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,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
|
||||||
@@ -182,6 +229,78 @@ public class OAuth2LoginTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-5562
|
||||||
|
@Test
|
||||||
|
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
|
||||||
|
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class,
|
||||||
|
OAuth2LoginWithCustomBeansConfig.class).autowire();
|
||||||
|
|
||||||
|
WebTestClient webTestClient = WebTestClientBuilder
|
||||||
|
.bindToWebFilters(this.springSecurity)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
|
||||||
|
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
|
||||||
|
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||||
|
OAuth2AccessToken.TokenType.BEARER, "openid", Instant.now(), Instant.now().plus(Duration.ofDays(1)));
|
||||||
|
OAuth2AuthorizationCodeAuthenticationToken authenticationToken =
|
||||||
|
new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
||||||
|
|
||||||
|
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
|
||||||
|
|
||||||
|
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||||
|
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
|
||||||
|
|
||||||
|
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error("invalid_request", "Invalid request", null);
|
||||||
|
when(tokenResponseClient.getTokenResponse(any())).thenThrow(new OAuth2AuthenticationException(oauth2Error));
|
||||||
|
|
||||||
|
webTestClient.get()
|
||||||
|
.uri("/login/oauth2/code/google")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.is3xxRedirection()
|
||||||
|
.expectHeader()
|
||||||
|
.valueEquals("Location", "/login?error");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class OAuth2LoginWithCustomBeansConfig {
|
||||||
|
|
||||||
|
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
|
||||||
|
|
||||||
|
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient =
|
||||||
|
mock(ReactiveOAuth2AccessTokenResponseClient.class);
|
||||||
|
|
||||||
|
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeExchange()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
.and()
|
||||||
|
.oauth2Login()
|
||||||
|
.authenticationConverter(authenticationConverter)
|
||||||
|
.authenticationManager(authenticationManager());
|
||||||
|
return http.build();
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactiveAuthenticationManager authenticationManager() {
|
||||||
|
OidcAuthorizationCodeReactiveAuthenticationManager oidc =
|
||||||
|
new OidcAuthorizationCodeReactiveAuthenticationManager(tokenResponseClient, userService);
|
||||||
|
return oidc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
|
||||||
|
return tokenResponseClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class GitHubWebFilter implements WebFilter {
|
static class GitHubWebFilter implements WebFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
-2
@@ -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();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
|
|||||||
* </property>
|
* </property>
|
||||||
* </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
-29
@@ -132,13 +132,18 @@ public class SessionRegistryImpl implements SessionRegistry,
|
|||||||
sessionIds.put(sessionId,
|
sessionIds.put(sessionId,
|
||||||
new SessionInformation(principal, sessionId, new Date()));
|
new SessionInformation(principal, sessionId, new Date()));
|
||||||
|
|
||||||
Set<String> sessionsUsedByPrincipal = principals.computeIfAbsent(principal, key -> new CopyOnWriteArraySet<>());
|
principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
|
||||||
sessionsUsedByPrincipal.add(sessionId);
|
if (sessionsUsedByPrincipal == null) {
|
||||||
|
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
|
||||||
|
}
|
||||||
|
sessionsUsedByPrincipal.add(sessionId);
|
||||||
|
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.trace("Sessions used by '" + principal + "' : "
|
logger.trace("Sessions used by '" + principal + "' : "
|
||||||
+ sessionsUsedByPrincipal);
|
+ sessionsUsedByPrincipal);
|
||||||
}
|
}
|
||||||
|
return sessionsUsedByPrincipal;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeSessionInformation(String sessionId) {
|
public void removeSessionInformation(String sessionId) {
|
||||||
@@ -157,32 +162,29 @@ public class SessionRegistryImpl implements SessionRegistry,
|
|||||||
|
|
||||||
sessionIds.remove(sessionId);
|
sessionIds.remove(sessionId);
|
||||||
|
|
||||||
Set<String> sessionsUsedByPrincipal = principals.get(info.getPrincipal());
|
principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
|
||||||
|
|
||||||
if (sessionsUsedByPrincipal == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("Removing session " + sessionId
|
|
||||||
+ " from principal's set of registered sessions");
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionsUsedByPrincipal.remove(sessionId);
|
|
||||||
|
|
||||||
if (sessionsUsedByPrincipal.isEmpty()) {
|
|
||||||
// No need to keep object in principals Map anymore
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug("Removing principal " + info.getPrincipal()
|
logger.debug("Removing session " + sessionId
|
||||||
+ " from registry");
|
+ " from principal's set of registered sessions");
|
||||||
}
|
}
|
||||||
principals.remove(info.getPrincipal());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger.isTraceEnabled()) {
|
sessionsUsedByPrincipal.remove(sessionId);
|
||||||
logger.trace("Sessions used by '" + info.getPrincipal() + "' : "
|
|
||||||
+ sessionsUsedByPrincipal);
|
if (sessionsUsedByPrincipal.isEmpty()) {
|
||||||
}
|
// No need to keep object in principals Map anymore
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Removing principal " + info.getPrincipal()
|
||||||
|
+ " from registry");
|
||||||
|
}
|
||||||
|
sessionsUsedByPrincipal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger.isTraceEnabled()) {
|
||||||
|
logger.trace("Sessions used by '" + info.getPrincipal() + "' : "
|
||||||
|
+ sessionsUsedByPrincipal);
|
||||||
|
}
|
||||||
|
return sessionsUsedByPrincipal;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+8
@@ -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));
|
||||||
|
|||||||
-2
@@ -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;
|
||||||
|
|||||||
+2
-1
@@ -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 {
|
||||||
|
|||||||
+11
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2873
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||||
|
|||||||
+1
-1
@@ -258,7 +258,7 @@ Or you might work for a company that has a legacy proprietary authentication sys
|
|||||||
In situations like this it's quite easy to get Spring Security to work, and still provide authorization capabilities.
|
In situations like this it's quite easy to get Spring Security to work, and still provide authorization capabilities.
|
||||||
All you need to do is write a filter (or equivalent) that reads the third-party user information from a location, build a Spring Security-specific `Authentication` object, and put it into the `SecurityContextHolder`.
|
All you need to do is write a filter (or equivalent) that reads the third-party user information from a location, build a Spring Security-specific `Authentication` object, and put it into the `SecurityContextHolder`.
|
||||||
In this case you also need to think about things which are normally taken care of automatically by the built-in authentication infrastructure.
|
In this case you also need to think about things which are normally taken care of automatically by the built-in authentication infrastructure.
|
||||||
For example, you might need to pre-emptively create an HTTP session to <<tech-intro-sec-context-persistence,cache the context between requests>>, before you write the response to the client footnote:[It isn't possible to create a session once the response has been committed.
|
For example, you might need to pre-emptively create an HTTP session to <<tech-intro-sec-context-persistence,cache the context between requests>>, before you write the response to the client footnote:[It isn't possible to create a session once the response has been committed.].
|
||||||
|
|
||||||
If you're wondering how the `AuthenticationManager` is implemented in a real world example, we'll look at that in the <<core-services-authentication-manager,core services chapter>>.
|
If you're wondering how the `AuthenticationManager` is implemented in a real world example, we'll look at that in the <<core-services-authentication-manager,core services chapter>>.
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
----
|
----
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import org.springframework.security.config.annotation.authentication.builders.*;
|
|||||||
import org.springframework.security.config.annotation.web.configuration.*;
|
import org.springframework.security.config.annotation.web.configuration.*;
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class WebSecurityConfig implements WebMvcConfigurer {
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public UserDetailsService userDetailsService() throws Exception {
|
public UserDetailsService userDetailsService() {
|
||||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||||
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
|
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
|
||||||
return manager;
|
return manager;
|
||||||
@@ -131,7 +131,10 @@ public class MvcWebApplicationInitializer extends
|
|||||||
== HttpSecurity
|
== HttpSecurity
|
||||||
|
|
||||||
Thus far our <<jc-hello-wsca,WebSecurityConfig>> only contains information about how to authenticate our users.
|
Thus far our <<jc-hello-wsca,WebSecurityConfig>> only contains information about how to authenticate our users.
|
||||||
How does Spring Security know that we want to require all users to be authenticated? How does Spring Security know we want to support form based authentication? The reason for this is that the `WebSecurityConfigurerAdapter` provides a default configuration in the `configure(HttpSecurity http)` method that looks like:
|
How does Spring Security know that we want to require all users to be authenticated?
|
||||||
|
How does Spring Security know we want to support form based authentication?
|
||||||
|
Actually, there is an configuration class that is being invoked behind the scenes called `WebSecurityConfigurerAdapter`.
|
||||||
|
It has a method called `configure` with the following default implementation:
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
@@ -172,9 +175,17 @@ I want to configure authorized requests __and__ configure form login __and__ con
|
|||||||
You might be wondering where the login form came from when you were prompted to log in, since we made no mention of any HTML files or JSPs.
|
You might be wondering where the login form came from when you were prompted to log in, since we made no mention of any HTML files or JSPs.
|
||||||
Since Spring Security's default configuration does not explicitly set a URL for the login page, Spring Security generates one automatically, based on the features that are enabled and using standard values for the URL which processes the submitted login, the default target URL the user will be sent to after logging in and so on.
|
Since Spring Security's default configuration does not explicitly set a URL for the login page, Spring Security generates one automatically, based on the features that are enabled and using standard values for the URL which processes the submitted login, the default target URL the user will be sent to after logging in and so on.
|
||||||
|
|
||||||
While the automatically generated log in page is convenient to get up and running quickly, most applications will want to provide their own log in page.
|
While the automatically generated log in page is convenient to get up and running quickly, most applications will want to provide their own login page.
|
||||||
To do so we can update our configuration as seen below:
|
When we want to change the default configuration, we can customize the `WebSecurityConfigurerAdapter` that we mentioned earlier by extending it like so:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
And then override the `configure` method as seen below:
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
@@ -1055,7 +1066,7 @@ public BCryptPasswordEncoder passwordEncoder() {
|
|||||||
== Multiple HttpSecurity
|
== Multiple HttpSecurity
|
||||||
|
|
||||||
We can configure multiple HttpSecurity instances just as we can have multiple `<http>` blocks.
|
We can configure multiple HttpSecurity instances just as we can have multiple `<http>` blocks.
|
||||||
The key is to extend the `WebSecurityConfigurationAdapter` multiple times.
|
The key is to extend the `WebSecurityConfigurerAdapter` multiple times.
|
||||||
For example, the following is an example of having a different configuration for URL's that start with `/api/`.
|
For example, the following is an example of having a different configuration for URL's that start with `/api/`.
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ With the default configuration, this is typically a comma-separated list of role
|
|||||||
The prefix "ROLE_" is a marker which indicates that a simple comparison with the user's authorities should be made.
|
The prefix "ROLE_" is a marker which indicates that a simple comparison with the user's authorities should be made.
|
||||||
In other words, a normal role-based check should be used.
|
In other words, a normal role-based check should be used.
|
||||||
Access-control in Spring Security is not limited to the use of simple roles (hence the use of the prefix to differentiate between different types of security attributes).
|
Access-control in Spring Security is not limited to the use of simple roles (hence the use of the prefix to differentiate between different types of security attributes).
|
||||||
We'll see later how the interpretation can vary footnote:[The interpretation of the comma-separated values in the `access` attribute depends on the implementation of the pass:specialcharacters,macros[<<ns-access-manager,AccessDecisionManager>>] which is used.
|
We'll see later how the interpretation can vary footnote:[The interpretation of the comma-separated values in the `access` attribute depends on the implementation of the <<ns-access-manager,AccessDecisionManager>> which is used.].
|
||||||
In Spring Security 3.0, the attribute can also be populated with an pass:specialcharacters,macros[<<el-access,EL expression>>].
|
In Spring Security 3.0, the attribute can also be populated with an pass:specialcharacters,macros[<<el-access,EL expression>>].
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
gaeVersion=1.9.71
|
gaeVersion=1.9.80
|
||||||
springBootVersion=2.1.6.RELEASE
|
springBootVersion=2.1.14.RELEASE
|
||||||
version=5.1.6.RELEASE
|
version=5.1.11.RELEASE
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
if (!project.hasProperty('reactorVersion')) {
|
if (!project.hasProperty('reactorVersion')) {
|
||||||
ext.reactorVersion = 'Californium-SR10'
|
ext.reactorVersion = 'Californium-SR18'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.hasProperty('springVersion')) {
|
if (!project.hasProperty('springVersion')) {
|
||||||
ext.springVersion = '5.1.9.RELEASE'
|
ext.springVersion = '5.1.15.RELEASE'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.hasProperty('springDataVersion')) {
|
if (!project.hasProperty('springDataVersion')) {
|
||||||
ext.springDataVersion = 'Lovelace-SR9'
|
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.3'
|
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.2'
|
dependency 'org.powermock:powermock-api-mockito2:2.0.7'
|
||||||
dependency 'org.powermock:powermock-api-support:2.0.2'
|
dependency 'org.powermock:powermock-api-support:2.0.7'
|
||||||
dependency 'org.powermock:powermock-core:2.0.2'
|
dependency 'org.powermock:powermock-core:2.0.7'
|
||||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.2'
|
dependency 'org.powermock:powermock-module-junit4-common:2.0.7'
|
||||||
dependency 'org.powermock:powermock-module-junit4:2.0.2'
|
dependency 'org.powermock:powermock-module-junit4:2.0.7'
|
||||||
dependency 'org.powermock:powermock-reflect:2.0.2'
|
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'
|
||||||
@@ -40,27 +40,27 @@ dependencyManagement {
|
|||||||
dependency 'asm:asm:3.1'
|
dependency 'asm:asm:3.1'
|
||||||
dependency 'ch.qos.logback:logback-classic:1.2.3'
|
dependency 'ch.qos.logback:logback-classic:1.2.3'
|
||||||
dependency 'ch.qos.logback:logback-core:1.2.3'
|
dependency 'ch.qos.logback:logback-core:1.2.3'
|
||||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.9'
|
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.10'
|
||||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.9.9'
|
dependency 'com.fasterxml.jackson.core:jackson-core:2.9.10'
|
||||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.9.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.2'
|
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.9'
|
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'
|
||||||
@@ -140,28 +140,28 @@ dependencyManagement {
|
|||||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:8.0.44'
|
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:8.0.44'
|
||||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
|
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
|
||||||
dependency 'org.apache.tomcat:tomcat-annotations-api:8.5.23'
|
dependency 'org.apache.tomcat:tomcat-annotations-api:8.5.23'
|
||||||
dependency 'org.aspectj:aspectjrt:1.9.2'
|
dependency 'org.aspectj:aspectjrt:1.9.5'
|
||||||
dependency 'org.aspectj:aspectjtools:1.9.2'
|
dependency 'org.aspectj:aspectjtools:1.9.5'
|
||||||
dependency 'org.aspectj:aspectjweaver:1.9.2'
|
dependency 'org.aspectj:aspectjweaver:1.9.5'
|
||||||
dependency 'org.assertj:assertj-core:3.11.1'
|
dependency 'org.assertj:assertj-core:3.11.1'
|
||||||
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
|
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
|
||||||
dependency 'org.bouncycastle:bcpkix-jdk15on:1.62'
|
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
|
||||||
dependency 'org.bouncycastle:bcprov-jdk15on:1.58'
|
dependency 'org.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.10.Final'
|
dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final'
|
||||||
dependency 'org.hibernate:hibernate-validator:6.0.17.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,17 +183,17 @@ 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'
|
||||||
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
|
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
|
||||||
dependency 'org.skyscreamer:jsonassert:1.5.0'
|
dependency 'org.skyscreamer:jsonassert:1.5.0'
|
||||||
dependency 'org.slf4j:jcl-over-slf4j:1.7.26'
|
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
|
||||||
dependency 'org.slf4j:jul-to-slf4j:1.7.26'
|
dependency 'org.slf4j:jul-to-slf4j:1.7.30'
|
||||||
dependency 'org.slf4j:log4j-over-slf4j:1.7.26'
|
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
|
||||||
dependency 'org.slf4j:slf4j-api:1.7.26'
|
dependency 'org.slf4j:slf4j-api:1.7.30'
|
||||||
dependency 'org.slf4j:slf4j-nop:1.7.26'
|
dependency 'org.slf4j:slf4j-nop:1.7.30'
|
||||||
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
|
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
|
||||||
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
|
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
|
||||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
|
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
|
||||||
|
|||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.ldap.server;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.ldap.core.ContextSource;
|
||||||
|
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
|
||||||
|
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link UnboundIdContainer}, specifically relating to LDIF file detection.
|
||||||
|
*
|
||||||
|
* @author Eleftheria Stein
|
||||||
|
*/
|
||||||
|
public class UnboundIdContainerLdifTests {
|
||||||
|
|
||||||
|
AnnotationConfigApplicationContext appCtx;
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void closeAppContext() {
|
||||||
|
if (appCtx != null) {
|
||||||
|
appCtx.close();
|
||||||
|
appCtx = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unboundIdContainerWhenCustomLdifNameThenLdifLoaded() {
|
||||||
|
appCtx = new AnnotationConfigApplicationContext(CustomLdifConfig.class);
|
||||||
|
|
||||||
|
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
|
||||||
|
.getBean(ContextSource.class);
|
||||||
|
|
||||||
|
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
|
||||||
|
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class CustomLdifConfig {
|
||||||
|
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||||
|
"classpath:test-server.ldif");
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
UnboundIdContainer ldapContainer() {
|
||||||
|
this.container.setPort(0);
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ContextSource contextSource(UnboundIdContainer container) {
|
||||||
|
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
|
||||||
|
+ container.getPort() + "/dc=springframework,dc=org");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
void shutdown() {
|
||||||
|
this.container.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unboundIdContainerWhenWildcardLdifNameThenLdifLoaded() {
|
||||||
|
appCtx = new AnnotationConfigApplicationContext(WildcardLdifConfig.class);
|
||||||
|
|
||||||
|
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
|
||||||
|
.getBean(ContextSource.class);
|
||||||
|
|
||||||
|
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
|
||||||
|
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class WildcardLdifConfig {
|
||||||
|
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||||
|
"classpath*:test-server.ldif");
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
UnboundIdContainer ldapContainer() {
|
||||||
|
this.container.setPort(0);
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ContextSource contextSource(UnboundIdContainer container) {
|
||||||
|
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
|
||||||
|
+ container.getPort() + "/dc=springframework,dc=org");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
void shutdown() {
|
||||||
|
this.container.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unboundIdContainerWhenMalformedLdifThenException() {
|
||||||
|
try {
|
||||||
|
appCtx = new AnnotationConfigApplicationContext(MalformedLdifConfig.class);
|
||||||
|
failBecauseExceptionWasNotThrown(IllegalStateException.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e.getCause()).isInstanceOf(IllegalStateException.class);
|
||||||
|
assertThat(e.getMessage()).contains("Unable to load LDIF classpath:test-server-malformed.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class MalformedLdifConfig {
|
||||||
|
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||||
|
"classpath:test-server-malformed.txt");
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
UnboundIdContainer ldapContainer() {
|
||||||
|
this.container.setPort(0);
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
void shutdown() {
|
||||||
|
this.container.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
dn: ou=groups,dc=springframework,dc=org
|
||||||
|
objectclass: top
|
||||||
|
objectclass: organizationalUnit
|
||||||
|
ou: groups
|
||||||
|
|
||||||
|
dn ou=subgroups,ou=groups,dc=springframework,dc=org
|
||||||
|
objectclass: top
|
||||||
|
objectclass: organizationalUnit
|
||||||
|
ou: subgroups
|
||||||
+17
-4
@@ -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
|
||||||
|
|||||||
@@ -114,10 +114,10 @@ public class UnboundIdContainer implements InitializingBean, DisposableBean, Lif
|
|||||||
|
|
||||||
private void importLdif(InMemoryDirectoryServer directoryServer) {
|
private void importLdif(InMemoryDirectoryServer directoryServer) {
|
||||||
if (StringUtils.hasText(this.ldif)) {
|
if (StringUtils.hasText(this.ldif)) {
|
||||||
Resource resource = this.context.getResource(this.ldif);
|
|
||||||
try {
|
try {
|
||||||
if (resource.exists()) {
|
Resource[] resources = this.context.getResources(this.ldif);
|
||||||
try (InputStream inputStream = resource.getInputStream()) {
|
if (resources.length > 0 && resources[0].exists()) {
|
||||||
|
try (InputStream inputStream = resources[0].getInputStream()) {
|
||||||
directoryServer.importFromLDIF(false, new LDIFReader(inputStream));
|
directoryServer.importFromLDIF(false, new LDIFReader(inputStream));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-8
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -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
-7
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
|
|||||||
*/
|
*/
|
||||||
final class OAuth2AuthorizationExchangeValidator {
|
final class OAuth2AuthorizationExchangeValidator {
|
||||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
|
||||||
|
|
||||||
static void validate(OAuth2AuthorizationExchange authorizationExchange) {
|
static void validate(OAuth2AuthorizationExchange authorizationExchange) {
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
|
OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
|
||||||
@@ -44,10 +43,5 @@ final class OAuth2AuthorizationExchangeValidator {
|
|||||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||||
throw new OAuth2AuthorizationException(oauth2Error);
|
throw new OAuth2AuthorizationException(oauth2Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
|
||||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
|
||||||
throw new OAuth2AuthorizationException(oauth2Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-23
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -15,7 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.oauth2.client.endpoint;
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
@@ -65,15 +68,18 @@ public class WebClientReactiveClientCredentialsTokenResponseClient implements Re
|
|||||||
.headers(headers(clientRegistration))
|
.headers(headers(clientRegistration))
|
||||||
.body(body)
|
.body(body)
|
||||||
.exchange()
|
.exchange()
|
||||||
.flatMap(response ->{
|
.flatMap(response -> {
|
||||||
if (!response.statusCode().is2xxSuccessful()){
|
HttpStatus status = HttpStatus.resolve(response.rawStatusCode());
|
||||||
|
if (status == null || !status.is2xxSuccessful()) {
|
||||||
// extract the contents of this into a method named oauth2AccessTokenResponse but has an argument for the response
|
// extract the contents of this into a method named oauth2AccessTokenResponse but has an argument for the response
|
||||||
throw WebClientResponseException.create(response.rawStatusCode(),
|
return response.bodyToFlux(DataBuffer.class)
|
||||||
|
.map(DataBufferUtils::release)
|
||||||
|
.then(Mono.error(WebClientResponseException.create(response.rawStatusCode(),
|
||||||
"Cannot get token, expected 2xx HTTP Status code",
|
"Cannot get token, expected 2xx HTTP Status code",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
)));
|
||||||
}
|
}
|
||||||
return response.body(oauth2AccessTokenResponse()); })
|
return response.body(oauth2AccessTokenResponse()); })
|
||||||
.map(response -> {
|
.map(response -> {
|
||||||
|
|||||||
-6
@@ -73,7 +73,6 @@ import org.springframework.util.StringUtils;
|
|||||||
*/
|
*/
|
||||||
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
|
||||||
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
||||||
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
|
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
|
||||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||||
@@ -127,11 +126,6 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
|
|||||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
|
||||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
|
||||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
OAuth2AccessTokenResponse accessTokenResponse;
|
OAuth2AccessTokenResponse accessTokenResponse;
|
||||||
try {
|
try {
|
||||||
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
||||||
|
|||||||
-6
@@ -76,7 +76,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
|||||||
ReactiveAuthenticationManager {
|
ReactiveAuthenticationManager {
|
||||||
|
|
||||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
|
||||||
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
||||||
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
|
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
|
||||||
|
|
||||||
@@ -127,11 +126,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
|||||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
|
||||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
|
||||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||||
authorizationCodeAuthentication.getClientRegistration(),
|
authorizationCodeAuthentication.getClientRegistration(),
|
||||||
authorizationCodeAuthentication.getAuthorizationExchange());
|
authorizationCodeAuthentication.getAuthorizationExchange());
|
||||||
|
|||||||
+32
-6
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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,15 +15,22 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.oauth2.client.registration;
|
package org.springframework.security.oauth2.client.registration;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Reactive {@link ClientRegistrationRepository} that stores {@link ClientRegistration}(s) in-memory.
|
* A Reactive {@link ClientRegistrationRepository} that stores {@link ClientRegistration}(s) in-memory.
|
||||||
*
|
*
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
|
* @author Ebert Toribio
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
* @see ClientRegistrationRepository
|
* @see ClientRegistrationRepository
|
||||||
* @see ClientRegistration
|
* @see ClientRegistration
|
||||||
@@ -31,7 +38,7 @@ import reactor.core.publisher.Mono;
|
|||||||
public final class InMemoryReactiveClientRegistrationRepository
|
public final class InMemoryReactiveClientRegistrationRepository
|
||||||
implements ReactiveClientRegistrationRepository, Iterable<ClientRegistration> {
|
implements ReactiveClientRegistrationRepository, Iterable<ClientRegistration> {
|
||||||
|
|
||||||
private final InMemoryClientRegistrationRepository delegate;
|
private final Map<String, ClientRegistration> clientIdToClientRegistration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an {@code InMemoryReactiveClientRegistrationRepository} using the provided parameters.
|
* Constructs an {@code InMemoryReactiveClientRegistrationRepository} using the provided parameters.
|
||||||
@@ -39,7 +46,12 @@ public final class InMemoryReactiveClientRegistrationRepository
|
|||||||
* @param registrations the client registration(s)
|
* @param registrations the client registration(s)
|
||||||
*/
|
*/
|
||||||
public InMemoryReactiveClientRegistrationRepository(ClientRegistration... registrations) {
|
public InMemoryReactiveClientRegistrationRepository(ClientRegistration... registrations) {
|
||||||
this.delegate = new InMemoryClientRegistrationRepository(registrations);
|
this(toList(registrations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ClientRegistration> toList(ClientRegistration... registrations) {
|
||||||
|
Assert.notEmpty(registrations, "registrations cannot be null or empty");
|
||||||
|
return Arrays.asList(registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,12 +60,12 @@ public final class InMemoryReactiveClientRegistrationRepository
|
|||||||
* @param registrations the client registration(s)
|
* @param registrations the client registration(s)
|
||||||
*/
|
*/
|
||||||
public InMemoryReactiveClientRegistrationRepository(List<ClientRegistration> registrations) {
|
public InMemoryReactiveClientRegistrationRepository(List<ClientRegistration> registrations) {
|
||||||
this.delegate = new InMemoryClientRegistrationRepository(registrations);
|
this.clientIdToClientRegistration = toUnmodifiableConcurrentMap(registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
|
public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
|
||||||
return Mono.justOrEmpty(this.delegate.findByRegistrationId(registrationId));
|
return Mono.justOrEmpty(this.clientIdToClientRegistration.get(registrationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +75,20 @@ public final class InMemoryReactiveClientRegistrationRepository
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Iterator<ClientRegistration> iterator() {
|
public Iterator<ClientRegistration> iterator() {
|
||||||
return delegate.iterator();
|
return this.clientIdToClientRegistration.values().iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, ClientRegistration> toUnmodifiableConcurrentMap(List<ClientRegistration> registrations) {
|
||||||
|
Assert.notEmpty(registrations, "registrations cannot be null or empty");
|
||||||
|
ConcurrentHashMap<String, ClientRegistration> result = new ConcurrentHashMap<>();
|
||||||
|
for (ClientRegistration registration : registrations) {
|
||||||
|
Assert.notNull(registration, "no registration can be null");
|
||||||
|
if (result.containsKey(registration.getRegistrationId())) {
|
||||||
|
throw new IllegalStateException(String.format("Duplicate key %s",
|
||||||
|
registration.getRegistrationId()));
|
||||||
|
}
|
||||||
|
result.put(registration.getRegistrationId(), registration);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-15
@@ -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
-1
@@ -133,7 +133,7 @@ class OAuth2AuthorizedClientResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<? extends OAuth2AuthorizedClient> clientCredentials(
|
Mono<OAuth2AuthorizedClient> clientCredentials(
|
||||||
ClientRegistration clientRegistration, Authentication authentication, ServerWebExchange exchange) {
|
ClientRegistration clientRegistration, Authentication authentication, ServerWebExchange exchange) {
|
||||||
OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
|
OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
|
||||||
return this.clientCredentialsTokenResponseClient.getTokenResponse(grantRequest)
|
return this.clientCredentialsTokenResponseClient.getTokenResponse(grantRequest)
|
||||||
|
|||||||
+27
-2
@@ -85,8 +85,12 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|||||||
private final OAuth2AuthorizedClientResolver authorizedClientResolver;
|
private final OAuth2AuthorizedClientResolver authorizedClientResolver;
|
||||||
|
|
||||||
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||||
|
this(authorizedClientRepository, new OAuth2AuthorizedClientResolver(clientRegistrationRepository, authorizedClientRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerOAuth2AuthorizedClientExchangeFilterFunction(ServerOAuth2AuthorizedClientRepository authorizedClientRepository, OAuth2AuthorizedClientResolver authorizedClientResolver) {
|
||||||
this.authorizedClientRepository = authorizedClientRepository;
|
this.authorizedClientRepository = authorizedClientRepository;
|
||||||
this.authorizedClientResolver = new OAuth2AuthorizedClientResolver(clientRegistrationRepository, authorizedClientRepository);
|
this.authorizedClientResolver = authorizedClientResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,13 +250,30 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Mono<OAuth2AuthorizedClient> refreshIfNecessary(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) {
|
private Mono<OAuth2AuthorizedClient> refreshIfNecessary(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) {
|
||||||
if (shouldRefresh(authorizedClient)) {
|
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
|
||||||
|
if (isClientCredentialsGrantType(clientRegistration) && hasTokenExpired(authorizedClient)) {
|
||||||
|
return createRequest(request)
|
||||||
|
.flatMap(r -> authorizeWithClientCredentials(clientRegistration, r));
|
||||||
|
} else if (shouldRefresh(authorizedClient)) {
|
||||||
return createRequest(request)
|
return createRequest(request)
|
||||||
.flatMap(r -> refreshAuthorizedClient(next, authorizedClient, r));
|
.flatMap(r -> refreshAuthorizedClient(next, authorizedClient, r));
|
||||||
}
|
}
|
||||||
return Mono.just(authorizedClient);
|
return Mono.just(authorizedClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isClientCredentialsGrantType(ClientRegistration clientRegistration) {
|
||||||
|
return AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<OAuth2AuthorizedClient> authorizeWithClientCredentials(ClientRegistration clientRegistration, OAuth2AuthorizedClientResolver.Request request) {
|
||||||
|
Authentication authentication = request.getAuthentication();
|
||||||
|
ServerWebExchange exchange = request.getExchange();
|
||||||
|
|
||||||
|
return this.authorizedClientResolver.clientCredentials(clientRegistration, authentication, exchange).
|
||||||
|
flatMap(result -> this.authorizedClientRepository.saveAuthorizedClient(result, authentication, exchange)
|
||||||
|
.thenReturn(result));
|
||||||
|
}
|
||||||
|
|
||||||
private Mono<OAuth2AuthorizedClient> refreshAuthorizedClient(ExchangeFunction next,
|
private Mono<OAuth2AuthorizedClient> refreshAuthorizedClient(ExchangeFunction next,
|
||||||
OAuth2AuthorizedClient authorizedClient, OAuth2AuthorizedClientResolver.Request r) {
|
OAuth2AuthorizedClient authorizedClient, OAuth2AuthorizedClientResolver.Request r) {
|
||||||
ServerWebExchange exchange = r.getExchange();
|
ServerWebExchange exchange = r.getExchange();
|
||||||
@@ -285,6 +306,10 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|||||||
if (refreshToken == null) {
|
if (refreshToken == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return hasTokenExpired(authorizedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTokenExpired(OAuth2AuthorizedClient authorizedClient) {
|
||||||
Instant now = this.clock.instant();
|
Instant now = this.clock.instant();
|
||||||
Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt();
|
Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt();
|
||||||
if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) {
|
if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) {
|
||||||
|
|||||||
+90
-32
@@ -22,6 +22,7 @@ import org.springframework.beans.factory.InitializingBean;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
@@ -103,6 +104,7 @@ import static org.springframework.security.oauth2.core.web.reactive.function.OAu
|
|||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
|
* @author Roman Matiushchenko
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
*/
|
*/
|
||||||
public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
||||||
@@ -146,7 +148,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterPropertiesSet() throws Exception {
|
||||||
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY, Operators.lift((s, sub) -> createRequestContextSubscriber(sub)));
|
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY, Operators.liftPublisher((s, sub) -> createRequestContextSubscriberIfNecessary(sub)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -319,14 +321,22 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void populateRequestAttributes(Map<String, Object> attrs, Context ctx) {
|
private void populateRequestAttributes(Map<String, Object> attrs, Context ctx) {
|
||||||
if (ctx.hasKey(HTTP_SERVLET_REQUEST_ATTR_NAME)) {
|
RequestContextDataHolder holder = RequestContextSubscriber.getRequestContext(ctx);
|
||||||
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, ctx.get(HTTP_SERVLET_REQUEST_ATTR_NAME));
|
if (holder != null) {
|
||||||
}
|
HttpServletRequest request = holder.getRequest();
|
||||||
if (ctx.hasKey(HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
|
if (request != null) {
|
||||||
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, ctx.get(HTTP_SERVLET_RESPONSE_ATTR_NAME));
|
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request);
|
||||||
}
|
}
|
||||||
if (ctx.hasKey(AUTHENTICATION_ATTR_NAME)) {
|
|
||||||
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, ctx.get(AUTHENTICATION_ATTR_NAME));
|
HttpServletResponse response = holder.getResponse();
|
||||||
|
if (response != null) {
|
||||||
|
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication authentication = holder.getAuthentication();
|
||||||
|
if (authentication != null) {
|
||||||
|
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, authentication);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
populateDefaultOAuth2AuthorizedClient(attrs);
|
populateDefaultOAuth2AuthorizedClient(attrs);
|
||||||
}
|
}
|
||||||
@@ -402,6 +412,10 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
throw new ClientAuthorizationRequiredException(clientRegistrationId);
|
throw new ClientAuthorizationRequiredException(clientRegistrationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isClientCredentialsGrantType(ClientRegistration clientRegistration) {
|
||||||
|
return AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType());
|
||||||
|
}
|
||||||
|
|
||||||
private OAuth2AuthorizedClient getAuthorizedClient(ClientRegistration clientRegistration,
|
private OAuth2AuthorizedClient getAuthorizedClient(ClientRegistration clientRegistration,
|
||||||
Map<String, Object> attrs) {
|
Map<String, Object> attrs) {
|
||||||
|
|
||||||
@@ -429,7 +443,11 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) {
|
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) {
|
||||||
if (shouldRefresh(authorizedClient)) {
|
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
|
||||||
|
if (isClientCredentialsGrantType(clientRegistration) && hasTokenExpired(authorizedClient)) {
|
||||||
|
//Client credentials grant do not have refresh tokens but can expire so we need to get another one
|
||||||
|
return Mono.fromSupplier(() -> getAuthorizedClient(clientRegistration, request.attributes()));
|
||||||
|
} else if (shouldRefresh(authorizedClient)) {
|
||||||
return refreshAuthorizedClient(request, next, authorizedClient);
|
return refreshAuthorizedClient(request, next, authorizedClient);
|
||||||
}
|
}
|
||||||
return Mono.just(authorizedClient);
|
return Mono.just(authorizedClient);
|
||||||
@@ -474,6 +492,10 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
if (refreshToken == null) {
|
if (refreshToken == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return hasTokenExpired(authorizedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTokenExpired(OAuth2AuthorizedClient authorizedClient) {
|
||||||
Instant now = this.clock.instant();
|
Instant now = this.clock.instant();
|
||||||
Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt();
|
Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt();
|
||||||
if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) {
|
if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) {
|
||||||
@@ -488,7 +510,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> CoreSubscriber<T> createRequestContextSubscriber(CoreSubscriber<T> delegate) {
|
<T> CoreSubscriber<T> createRequestContextSubscriberIfNecessary(CoreSubscriber<T> delegate) {
|
||||||
HttpServletRequest request = null;
|
HttpServletRequest request = null;
|
||||||
HttpServletResponse response = null;
|
HttpServletResponse response = null;
|
||||||
ServletRequestAttributes requestAttributes =
|
ServletRequestAttributes requestAttributes =
|
||||||
@@ -498,6 +520,10 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
response = requestAttributes.getResponse();
|
response = requestAttributes.getResponse();
|
||||||
}
|
}
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null && request == null && response == null) {
|
||||||
|
//do not need to create RequestContextSubscriber with empty data
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
return new RequestContextSubscriber<>(delegate, request, response, authentication);
|
return new RequestContextSubscriber<>(delegate, request, response, authentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,34 +601,37 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class RequestContextSubscriber<T> implements CoreSubscriber<T> {
|
static class RequestContextSubscriber<T> implements CoreSubscriber<T> {
|
||||||
private static final String CONTEXT_DEFAULTED_ATTR_NAME = RequestContextSubscriber.class.getName().concat(".CONTEXT_DEFAULTED_ATTR_NAME");
|
static final String REQUEST_CONTEXT_DATA_HOLDER =
|
||||||
|
RequestContextSubscriber.class.getName().concat(".REQUEST_CONTEXT_DATA_HOLDER");
|
||||||
private final CoreSubscriber<T> delegate;
|
private final CoreSubscriber<T> delegate;
|
||||||
private final HttpServletRequest request;
|
private final Context context;
|
||||||
private final HttpServletResponse response;
|
|
||||||
private final Authentication authentication;
|
|
||||||
|
|
||||||
private RequestContextSubscriber(CoreSubscriber<T> delegate,
|
RequestContextSubscriber(CoreSubscriber<T> delegate,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
this.request = request;
|
|
||||||
this.response = response;
|
Context parentContext = this.delegate.currentContext();
|
||||||
this.authentication = authentication;
|
Context context;
|
||||||
|
if (parentContext.hasKey(REQUEST_CONTEXT_DATA_HOLDER)) {
|
||||||
|
context = parentContext;
|
||||||
|
} else {
|
||||||
|
context = parentContext.put(REQUEST_CONTEXT_DATA_HOLDER, new RequestContextDataHolder(request, response, authentication));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static RequestContextDataHolder getRequestContext(Context ctx) {
|
||||||
|
return ctx.getOrDefault(REQUEST_CONTEXT_DATA_HOLDER, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Context currentContext() {
|
public Context currentContext() {
|
||||||
Context context = this.delegate.currentContext();
|
return this.context;
|
||||||
if (context.hasKey(CONTEXT_DEFAULTED_ATTR_NAME)) {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
return Context.of(
|
|
||||||
CONTEXT_DEFAULTED_ATTR_NAME, Boolean.TRUE,
|
|
||||||
HTTP_SERVLET_REQUEST_ATTR_NAME, this.request,
|
|
||||||
HTTP_SERVLET_RESPONSE_ATTR_NAME, this.response,
|
|
||||||
AUTHENTICATION_ATTR_NAME, this.authentication);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -625,4 +654,33 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
|||||||
this.delegate.onComplete();
|
this.delegate.onComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class RequestContextDataHolder {
|
||||||
|
private final HttpServletRequest request;
|
||||||
|
private final HttpServletResponse response;
|
||||||
|
private final Authentication authentication;
|
||||||
|
|
||||||
|
RequestContextDataHolder(@Nullable HttpServletRequest request,
|
||||||
|
@Nullable HttpServletResponse response,
|
||||||
|
@Nullable Authentication authentication) {
|
||||||
|
this.request = request;
|
||||||
|
this.response = response;
|
||||||
|
this.authentication = authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private HttpServletRequest getRequest() {
|
||||||
|
return this.request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private HttpServletResponse getResponse() {
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Authentication getAuthentication() {
|
||||||
|
return this.authentication;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-7
@@ -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,14 +35,22 @@ import org.springframework.security.web.server.authentication.RedirectServerAuth
|
|||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
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 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.
|
||||||
@@ -71,6 +79,7 @@ import reactor.core.publisher.Mono;
|
|||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
|
* @author Joe Grandja
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
* @see OAuth2AuthorizationCodeAuthenticationToken
|
* @see OAuth2AuthorizationCodeAuthenticationToken
|
||||||
* @see org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager
|
* @see org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager
|
||||||
@@ -89,6 +98,9 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
|||||||
|
|
||||||
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||||
|
|
||||||
|
private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
|
||||||
|
new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||||
|
|
||||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||||
|
|
||||||
private ServerAuthenticationConverter authenticationConverter;
|
private ServerAuthenticationConverter authenticationConverter;
|
||||||
@@ -109,7 +121,7 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
|||||||
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
this.authorizedClientRepository = authorizedClientRepository;
|
this.authorizedClientRepository = authorizedClientRepository;
|
||||||
this.requiresAuthenticationMatcher = new PathPatternParserServerWebExchangeMatcher("/{action}/oauth2/code/{registrationId}");
|
this.requiresAuthenticationMatcher = this::matchesAuthorizationResponse;
|
||||||
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
||||||
this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
||||||
this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
|
this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
|
||||||
@@ -124,7 +136,7 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
|||||||
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
this.authorizedClientRepository = authorizedClientRepository;
|
this.authorizedClientRepository = authorizedClientRepository;
|
||||||
this.requiresAuthenticationMatcher = new PathPatternParserServerWebExchangeMatcher("/{action}/oauth2/code/{registrationId}");
|
this.requiresAuthenticationMatcher = this::matchesAuthorizationResponse;
|
||||||
this.authenticationConverter = authenticationConverter;
|
this.authenticationConverter = authenticationConverter;
|
||||||
this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
||||||
this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
|
this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
|
||||||
@@ -133,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,
|
||||||
@@ -164,4 +176,36 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
|||||||
.flatMap(principal -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, webFilterExchange.getExchange()))
|
.flatMap(principal -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, webFilterExchange.getExchange()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
|
||||||
|
return Mono.just(exchange)
|
||||||
|
.filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
|
||||||
|
.flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||||
|
.flatMap(authorizationRequest ->
|
||||||
|
matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
|
||||||
|
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mono<ServerWebExchangeMatcher.MatchResult> matchesRedirectUri(
|
||||||
|
URI authorizationResponseUri, String authorizationRequestRedirectUri) {
|
||||||
|
UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build();
|
||||||
|
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build();
|
||||||
|
Set<Map.Entry<String, List<String>>> requestUriParameters =
|
||||||
|
new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
|
||||||
|
Set<Map.Entry<String, List<String>>> redirectUriParameters =
|
||||||
|
new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
|
||||||
|
// Remove the additional request parameters (if any) from the authorization response (request)
|
||||||
|
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
|
||||||
|
requestUriParameters.retainAll(redirectUriParameters);
|
||||||
|
|
||||||
|
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
|
||||||
|
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
|
||||||
|
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
|
||||||
|
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
|
||||||
|
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
|
||||||
|
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
|
||||||
|
return ServerWebExchangeMatcher.MatchResult.match();
|
||||||
|
}
|
||||||
|
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-7
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -85,6 +85,9 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
|
|||||||
OAuth2AuthorizationRequest removedValue = stateToAuthzRequest.remove(state);
|
OAuth2AuthorizationRequest removedValue = stateToAuthzRequest.remove(state);
|
||||||
if (stateToAuthzRequest.isEmpty()) {
|
if (stateToAuthzRequest.isEmpty()) {
|
||||||
sessionAttrs.remove(this.sessionAttributeName);
|
sessionAttrs.remove(this.sessionAttributeName);
|
||||||
|
} else if (removedValue != null) {
|
||||||
|
// gh-7327 Overwrite the existing Map to ensure the state is saved for distributed sessions
|
||||||
|
sessionAttrs.put(this.sessionAttributeName, stateToAuthzRequest);
|
||||||
}
|
}
|
||||||
if (removedValue == null) {
|
if (removedValue == null) {
|
||||||
sink.complete();
|
sink.complete();
|
||||||
|
|||||||
+28
-15
@@ -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.
|
||||||
@@ -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}.
|
||||||
@@ -108,18 +111,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
|
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthorizationException() {
|
|
||||||
when(this.authorizationRequest.getRedirectUri()).thenReturn("https://example.com");
|
|
||||||
when(this.authorizationResponse.getRedirectUri()).thenReturn("https://example2.com");
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> {
|
|
||||||
this.authenticationProvider.authenticate(
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
this.clientRegistration, this.authorizationExchange));
|
|
||||||
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
|
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
|
||||||
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
|
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
|
||||||
@@ -142,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
-8
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -80,13 +80,6 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests {
|
|||||||
.isInstanceOf(OAuth2AuthorizationException.class);
|
.isInstanceOf(OAuth2AuthorizationException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenRedirectUriNotEqualThenOAuth2AuthorizationException() {
|
|
||||||
this.authorizationRequest.redirectUri("https://example.org/notequal");
|
|
||||||
assertThatCode(() -> authenticate())
|
|
||||||
.isInstanceOf(OAuth2AuthorizationException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenValidThenSuccess() {
|
public void authenticateWhenValidThenSuccess() {
|
||||||
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build()));
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build()));
|
||||||
|
|||||||
+1
-13
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -154,18 +154,6 @@ public class OAuth2LoginAuthenticationProviderTests {
|
|||||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
|
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
|
|
||||||
this.exception.expect(OAuth2AuthenticationException.class);
|
|
||||||
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
|
|
||||||
|
|
||||||
when(this.authorizationRequest.getRedirectUri()).thenReturn("https://example.com");
|
|
||||||
when(this.authorizationResponse.getRedirectUri()).thenReturn("https://example2.com");
|
|
||||||
|
|
||||||
this.authenticationProvider.authenticate(
|
|
||||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
|
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
|
||||||
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
|
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
|
||||||
|
|||||||
+1
-13
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -169,18 +169,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|||||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
|
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
|
|
||||||
this.exception.expect(OAuth2AuthenticationException.class);
|
|
||||||
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
|
|
||||||
|
|
||||||
when(this.authorizationRequest.getRedirectUri()).thenReturn("https://example1.com");
|
|
||||||
when(this.authorizationResponse.getRedirectUri()).thenReturn("https://example2.com");
|
|
||||||
|
|
||||||
this.authenticationProvider.authenticate(
|
|
||||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
|
||||||
this.exception.expect(OAuth2AuthenticationException.class);
|
this.exception.expect(OAuth2AuthenticationException.class);
|
||||||
|
|||||||
+191
-127
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+87
@@ -44,6 +44,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
|
|||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.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.client.web.reactive.function.client.OAuth2AuthorizedClientResolver.Request;
|
||||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
@@ -69,6 +70,7 @@ import java.util.Optional;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -88,6 +90,9 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
|||||||
@Mock
|
@Mock
|
||||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OAuth2AuthorizedClientResolver oAuth2AuthorizedClientResolver;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ServerWebExchange serverWebExchange;
|
private ServerWebExchange serverWebExchange;
|
||||||
|
|
||||||
@@ -149,6 +154,88 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
|||||||
assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue());
|
assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() {
|
||||||
|
TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this");
|
||||||
|
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
|
||||||
|
String clientRegistrationId = registration.getClientId();
|
||||||
|
|
||||||
|
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository, this.oAuth2AuthorizedClientResolver);
|
||||||
|
|
||||||
|
OAuth2AccessToken newAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
"new-token",
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plus(Duration.ofDays(1)));
|
||||||
|
OAuth2AuthorizedClient newAuthorizedClient = new OAuth2AuthorizedClient(registration,
|
||||||
|
"principalName", newAccessToken, null);
|
||||||
|
Request r = new Request(clientRegistrationId, authentication, null);
|
||||||
|
when(this.oAuth2AuthorizedClientResolver.clientCredentials(any(), any(), any())).thenReturn(Mono.just(newAuthorizedClient));
|
||||||
|
when(this.oAuth2AuthorizedClientResolver.createDefaultedRequest(any(), any(), any())).thenReturn(Mono.just(r));
|
||||||
|
|
||||||
|
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
|
||||||
|
Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1));
|
||||||
|
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(),
|
||||||
|
this.accessToken.getTokenValue(),
|
||||||
|
issuedAt,
|
||||||
|
accessTokenExpiresAt);
|
||||||
|
|
||||||
|
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(registration,
|
||||||
|
"principalName", accessToken, null);
|
||||||
|
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||||
|
.attributes(oauth2AuthorizedClient(authorizedClient))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
this.function.filter(request, this.exchange)
|
||||||
|
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any());
|
||||||
|
verify(this.oAuth2AuthorizedClientResolver).clientCredentials(any(), any(), any());
|
||||||
|
verify(this.oAuth2AuthorizedClientResolver).createDefaultedRequest(any(), any(), any());
|
||||||
|
|
||||||
|
List<ClientRequest> requests = this.exchange.getRequests();
|
||||||
|
assertThat(requests).hasSize(1);
|
||||||
|
ClientRequest request1 = requests.get(0);
|
||||||
|
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
|
||||||
|
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||||
|
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||||
|
assertThat(getBody(request1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterWhenClientCredentialsTokenNotExpiredThenUseCurrentToken() {
|
||||||
|
TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this");
|
||||||
|
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
|
||||||
|
|
||||||
|
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository, this.oAuth2AuthorizedClientResolver);
|
||||||
|
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(registration,
|
||||||
|
"principalName", this.accessToken, null);
|
||||||
|
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||||
|
.attributes(oauth2AuthorizedClient(authorizedClient))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.function.filter(request, this.exchange)
|
||||||
|
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(this.oAuth2AuthorizedClientResolver, never()).clientCredentials(any(), any(), any());
|
||||||
|
verify(this.oAuth2AuthorizedClientResolver, never()).createDefaultedRequest(any(), any(), any());
|
||||||
|
|
||||||
|
List<ClientRequest> requests = this.exchange.getRequests();
|
||||||
|
assertThat(requests).hasSize(1);
|
||||||
|
ClientRequest request1 = requests.get(0);
|
||||||
|
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0");
|
||||||
|
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||||
|
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||||
|
assertThat(getBody(request1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void filterWhenRefreshRequiredThenRefresh() {
|
public void filterWhenRefreshRequiredThenRefresh() {
|
||||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
|
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
|
||||||
|
|||||||
+170
-2
@@ -55,13 +55,17 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepo
|
|||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.reactive.function.BodyInserter;
|
import org.springframework.web.reactive.function.BodyInserter;
|
||||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.CoreSubscriber;
|
||||||
|
import reactor.core.publisher.BaseSubscriber;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.context.Context;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -74,9 +78,14 @@ import java.util.Optional;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.http.HttpMethod.GET;
|
import static org.springframework.http.HttpMethod.GET;
|
||||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*;
|
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*;
|
||||||
|
|
||||||
@@ -124,9 +133,10 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void cleanup() {
|
public void cleanup() throws Exception {
|
||||||
SecurityContextHolder.clearContext();
|
SecurityContextHolder.clearContext();
|
||||||
RequestContextHolder.resetRequestAttributes();
|
RequestContextHolder.resetRequestAttributes();
|
||||||
|
this.function.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -428,6 +438,80 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
|||||||
assertThat(getBody(request1)).isEmpty();
|
assertThat(getBody(request1)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterWhenClientCredentialsTokenNotExpiredThenUseCurrentToken() {
|
||||||
|
this.registration = TestClientRegistrations.clientCredentials().build();
|
||||||
|
|
||||||
|
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository,
|
||||||
|
this.authorizedClientRepository);
|
||||||
|
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
|
||||||
|
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
|
||||||
|
"principalName", this.accessToken, null);
|
||||||
|
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||||
|
.attributes(oauth2AuthorizedClient(authorizedClient))
|
||||||
|
.attributes(authentication(this.authentication))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.function.filter(request, this.exchange).block();
|
||||||
|
|
||||||
|
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), eq(this.authentication), any(), any());
|
||||||
|
|
||||||
|
verify(clientCredentialsTokenResponseClient, never()).getTokenResponse(any());
|
||||||
|
|
||||||
|
List<ClientRequest> requests = this.exchange.getRequests();
|
||||||
|
assertThat(requests).hasSize(1);
|
||||||
|
|
||||||
|
ClientRequest request1 = requests.get(0);
|
||||||
|
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0");
|
||||||
|
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||||
|
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||||
|
assertThat(getBody(request1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterWhenClientCredentialsTokenExpiredThenGetNewToken() {
|
||||||
|
this.registration = TestClientRegistrations.clientCredentials().build();
|
||||||
|
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses
|
||||||
|
.accessTokenResponse().build();
|
||||||
|
when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(
|
||||||
|
accessTokenResponse);
|
||||||
|
|
||||||
|
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
|
||||||
|
Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1));
|
||||||
|
|
||||||
|
this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(),
|
||||||
|
this.accessToken.getTokenValue(),
|
||||||
|
issuedAt,
|
||||||
|
accessTokenExpiresAt);
|
||||||
|
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository,
|
||||||
|
this.authorizedClientRepository);
|
||||||
|
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
|
||||||
|
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
|
||||||
|
"principalName", this.accessToken, null);
|
||||||
|
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||||
|
.attributes(oauth2AuthorizedClient(authorizedClient))
|
||||||
|
.attributes(authentication(this.authentication))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.function.filter(request, this.exchange).block();
|
||||||
|
|
||||||
|
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(this.authentication), any(), any());
|
||||||
|
|
||||||
|
verify(clientCredentialsTokenResponseClient).getTokenResponse(any());
|
||||||
|
|
||||||
|
List<ClientRequest> requests = this.exchange.getRequests();
|
||||||
|
assertThat(requests).hasSize(1);
|
||||||
|
|
||||||
|
ClientRequest request1 = requests.get(0);
|
||||||
|
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token");
|
||||||
|
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||||
|
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||||
|
assertThat(getBody(request1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() {
|
public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() {
|
||||||
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
|
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
|
||||||
@@ -636,6 +720,90 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
|||||||
assertThat(getBody(request)).isEmpty();
|
assertThat(getBody(request)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-7228
|
||||||
|
@Test
|
||||||
|
public void afterPropertiesSetWhenHooksInitAndOutsideWebSecurityContextThenShouldNotThrowException() throws Exception {
|
||||||
|
this.function.afterPropertiesSet(); // Hooks.onLastOperator() initialized
|
||||||
|
assertThatCode(() -> Mono.subscriberContext().block())
|
||||||
|
.as("RequestContext Hook brakes application outside of web/security context")
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createRequestContextSubscriberIfNecessaryWhenOutsideWebSecurityContextThenReturnOriginalSubscriber() throws Exception {
|
||||||
|
BaseSubscriber<Object> originalSubscriber = new BaseSubscriber<Object>() {};
|
||||||
|
CoreSubscriber<Object> resultSubscriber = this.function.createRequestContextSubscriberIfNecessary(originalSubscriber);
|
||||||
|
assertThat(resultSubscriber).isSameAs(originalSubscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7228
|
||||||
|
@Test
|
||||||
|
public void createRequestContextSubscriberWhenRequestResponseProvidedThenCreateWithParentContext() throws Exception {
|
||||||
|
testRequestContextSubscriber(new MockHttpServletRequest(), new MockHttpServletResponse(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7228
|
||||||
|
@Test
|
||||||
|
public void createRequestContextSubscriberWhenAuthenticationProvidedThenCreateWithParentContext() throws Exception {
|
||||||
|
testRequestContextSubscriber(null, null, this.authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createRequestContextSubscriberWhenParentContextHasDataHolderThenShouldReuseParentContext() throws Exception {
|
||||||
|
RequestContextDataHolder testValue = new RequestContextDataHolder(null, null, null);
|
||||||
|
final Context parentContext = Context.of(RequestContextSubscriber.REQUEST_CONTEXT_DATA_HOLDER, testValue);
|
||||||
|
BaseSubscriber<Object> parent = new BaseSubscriber<Object>() {
|
||||||
|
@Override
|
||||||
|
public Context currentContext() {
|
||||||
|
return parentContext;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RequestContextSubscriber<Object> requestContextSubscriber =
|
||||||
|
new RequestContextSubscriber<>(parent, null, null, authentication);
|
||||||
|
|
||||||
|
Context resultContext = requestContextSubscriber.currentContext();
|
||||||
|
|
||||||
|
assertThat(resultContext)
|
||||||
|
.describedAs("parent context was replaced")
|
||||||
|
.isSameAs(parentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testRequestContextSubscriber(MockHttpServletRequest servletRequest,
|
||||||
|
MockHttpServletResponse servletResponse,
|
||||||
|
Authentication authentication) {
|
||||||
|
String testKey = "test_key";
|
||||||
|
String testValue = "test_value";
|
||||||
|
|
||||||
|
BaseSubscriber<Object> parent = new BaseSubscriber<Object>() {
|
||||||
|
@Override
|
||||||
|
public Context currentContext() {
|
||||||
|
return Context.of(testKey, testValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RequestContextSubscriber<Object> requestContextSubscriber =
|
||||||
|
new RequestContextSubscriber<>(parent, servletRequest, servletResponse, authentication);
|
||||||
|
|
||||||
|
Context resultContext = requestContextSubscriber.currentContext();
|
||||||
|
|
||||||
|
assertThat(resultContext)
|
||||||
|
.describedAs("result context is null")
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
|
assertThat(resultContext.getOrEmpty(testKey))
|
||||||
|
.describedAs("context is replaced")
|
||||||
|
.hasValue(testValue);
|
||||||
|
|
||||||
|
Object dataHolder = resultContext.getOrDefault(RequestContextSubscriber.REQUEST_CONTEXT_DATA_HOLDER, null);
|
||||||
|
assertThat(dataHolder)
|
||||||
|
.describedAs("context is not populated with REQUEST_CONTEXT_DATA_HOLDER")
|
||||||
|
.isNotNull()
|
||||||
|
.hasFieldOrPropertyWithValue("request", servletRequest)
|
||||||
|
.hasFieldOrPropertyWithValue("response", servletResponse)
|
||||||
|
.hasFieldOrPropertyWithValue("authentication", authentication);
|
||||||
|
}
|
||||||
|
|
||||||
private static String getBody(ClientRequest request) {
|
private static String getBody(ClientRequest request) {
|
||||||
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
||||||
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
||||||
|
|||||||
+160
-19
@@ -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.
|
||||||
@@ -25,19 +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.ReactiveClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
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 static org.assertj.core.api.Assertions.*;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
@@ -53,6 +62,9 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
|||||||
@Mock
|
@Mock
|
||||||
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||||
|
|
||||||
|
private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
|
||||||
|
new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
|
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
|
||||||
@@ -92,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();
|
||||||
|
|
||||||
@@ -101,25 +113,154 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void filterWhenMatchThenAuthorizedClientSaved() {
|
public void filterWhenMatchThenAuthorizedClientSaved() {
|
||||||
Mono<Authentication> authentication = Mono
|
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||||
.just(TestOAuth2AuthorizationCodeAuthenticationTokens.unauthenticated());
|
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens
|
.thenReturn(Mono.just(clientRegistration));
|
||||||
.authenticated();
|
|
||||||
ServerAuthenticationConverter converter = e -> authentication;
|
|
||||||
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
|
|
||||||
this.authenticationManager, converter, this.authorizedClientRepository);
|
|
||||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest
|
|
||||||
.get("/authorize/oauth2/code/registration-id"));
|
|
||||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
|
||||||
e -> e.getResponse().setComplete());
|
|
||||||
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(
|
|
||||||
authenticated));
|
|
||||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
||||||
.thenReturn(Mono.empty());
|
.thenReturn(Mono.empty());
|
||||||
|
when(this.authenticationManager.authenticate(any()))
|
||||||
|
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
|
||||||
|
|
||||||
|
MockServerHttpRequest authorizationRequest =
|
||||||
|
createAuthorizationRequest("/authorization/callback");
|
||||||
|
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();
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-32
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -63,7 +63,7 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
|||||||
.queryParam(OAuth2ParameterNames.STATE, "state"));
|
.queryParam(OAuth2ParameterNames.STATE, "state"));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loadAuthorizatioNRequestWhenNullExchangeThenIllegalArgumentException() {
|
public void loadAuthorizationRequestWhenNullExchangeThenIllegalArgumentException() {
|
||||||
this.exchange = null;
|
this.exchange = null;
|
||||||
assertThatThrownBy(() -> this.repository.loadAuthorizationRequest(this.exchange))
|
assertThatThrownBy(() -> this.repository.loadAuthorizationRequest(this.exchange))
|
||||||
.isInstanceOf(IllegalArgumentException.class);
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
@@ -106,36 +106,6 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
|||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void multipleSavedAuthorizationRequestAndRedisCookie() {
|
|
||||||
String oldState = "state0";
|
|
||||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
|
||||||
.queryParam(OAuth2ParameterNames.STATE, oldState).build();
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
|
||||||
.authorizationUri("https://example.com/oauth2/authorize")
|
|
||||||
.clientId("client-id")
|
|
||||||
.redirectUri("http://localhost/client-1")
|
|
||||||
.state(oldState)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Map<String, Object> sessionAttrs = spy(new HashMap<>());
|
|
||||||
WebSession session = mock(WebSession.class);
|
|
||||||
when(session.getAttributes()).thenReturn(sessionAttrs);
|
|
||||||
WebSessionManager sessionManager = e -> Mono.just(session);
|
|
||||||
|
|
||||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(), sessionManager,
|
|
||||||
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
|
||||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(), sessionManager,
|
|
||||||
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
|
||||||
|
|
||||||
Mono<Void> saveAndSave = this.repository.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
|
||||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange));
|
|
||||||
|
|
||||||
StepVerifier.create(saveAndSave).verifyComplete();
|
|
||||||
verify(sessionAttrs, times(2)).put(any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
|
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
|
||||||
String oldState = "state0";
|
String oldState = "state0";
|
||||||
@@ -269,6 +239,44 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
|||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-7327
|
||||||
|
@Test
|
||||||
|
public void removeAuthorizationRequestWhenMultipleThenRemovedAndSessionAttributeUpdated() {
|
||||||
|
String oldState = "state0";
|
||||||
|
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||||
|
.queryParam(OAuth2ParameterNames.STATE, oldState).build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||||
|
.authorizationUri("https://example.com/oauth2/authorize")
|
||||||
|
.clientId("client-id")
|
||||||
|
.redirectUri("http://localhost/client-1")
|
||||||
|
.state(oldState)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Map<String, Object> sessionAttrs = spy(new HashMap<>());
|
||||||
|
WebSession session = mock(WebSession.class);
|
||||||
|
when(session.getAttributes()).thenReturn(sessionAttrs);
|
||||||
|
WebSessionManager sessionManager = e -> Mono.just(session);
|
||||||
|
|
||||||
|
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(), sessionManager,
|
||||||
|
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||||
|
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(), sessionManager,
|
||||||
|
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||||
|
|
||||||
|
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||||
|
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||||
|
.then(this.repository.removeAuthorizationRequest(this.exchange));
|
||||||
|
|
||||||
|
StepVerifier.create(saveAndSaveAndRemove)
|
||||||
|
.expectNext(this.authorizationRequest)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(sessionAttrs, times(3)).put(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
private void assertSessionStartedIs(boolean expected) {
|
private void assertSessionStartedIs(boolean expected) {
|
||||||
Mono<Boolean> isStarted = this.exchange.getSession().map(WebSession::isStarted);
|
Mono<Boolean> isStarted = this.exchange.getSession().map(WebSession::isStarted);
|
||||||
StepVerifier.create(isStarted)
|
StepVerifier.create(isStarted)
|
||||||
|
|||||||
+11
-5
@@ -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);
|
||||||
|
|||||||
+12
-5
@@ -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);
|
||||||
|
|||||||
+34
-1
@@ -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);
|
||||||
|
|||||||
+20
-1
@@ -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);
|
||||||
|
|||||||
+3
-1
@@ -20,6 +20,7 @@ import java.net.MalformedURLException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -210,12 +211,13 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class RestOperationsResourceRetriever implements ResourceRetriever {
|
private static class RestOperationsResourceRetriever implements ResourceRetriever {
|
||||||
|
private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
|
||||||
private RestOperations restOperations = new RestTemplate();
|
private RestOperations restOperations = new RestTemplate();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Resource retrieveResource(URL url) throws IOException {
|
public Resource retrieveResource(URL url) throws IOException {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
|
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
|
||||||
|
|
||||||
ResponseEntity<String> response;
|
ResponseEntity<String> response;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+23
@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.jwt;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm;
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
@@ -31,16 +32,21 @@ import okhttp3.mockwebserver.MockWebServer;
|
|||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||||
import org.powermock.modules.junit4.PowerMockRunner;
|
import org.powermock.modules.junit4.PowerMockRunner;
|
||||||
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.RequestEntity;
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||||
|
import org.springframework.web.client.RestOperations;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -76,6 +82,8 @@ public class NimbusJwtDecoderJwkSupportTests {
|
|||||||
private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A";
|
private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A";
|
||||||
private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
|
private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
|
||||||
|
|
||||||
|
private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
|
||||||
|
|
||||||
private NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM);
|
private NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -256,4 +264,19 @@ public class NimbusJwtDecoderJwkSupportTests {
|
|||||||
assertThatCode(() -> jwtDecoder.setClaimSetConverter(null))
|
assertThatCode(() -> jwtDecoder.setClaimSetConverter(null))
|
||||||
.isInstanceOf(IllegalArgumentException.class);
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-7290
|
||||||
|
@Test
|
||||||
|
public void decodeWhenJwkSetRequestedThenAcceptHeaderJsonAndJwkSetJson() {
|
||||||
|
RestOperations restOperations = mock(RestOperations.class);
|
||||||
|
when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||||
|
.thenReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||||
|
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL);
|
||||||
|
jwtDecoder.setRestOperations(restOperations);
|
||||||
|
jwtDecoder.decode(SIGNED_JWT);
|
||||||
|
ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class);
|
||||||
|
verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class));
|
||||||
|
List<MediaType> acceptHeader = requestEntityCaptor.getValue().getHeaders().getAccept();
|
||||||
|
assertThat(acceptHeader).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-37
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -255,42 +255,6 @@ public class OAuth2LoginApplicationTests {
|
|||||||
assertThat(errorElement.asText()).contains("authorization_request_not_found");
|
assertThat(errorElement.asText()).contains("authorization_request_not_found");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
|
|
||||||
HtmlPage page = this.webClient.getPage("/");
|
|
||||||
URL loginPageUrl = page.getBaseURL();
|
|
||||||
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
|
||||||
|
|
||||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google");
|
|
||||||
|
|
||||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, clientRegistration);
|
|
||||||
assertThat(clientAnchorElement).isNotNull();
|
|
||||||
|
|
||||||
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
|
||||||
|
|
||||||
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
|
|
||||||
URI.create(response.getResponseHeaderValue("Location"))).build();
|
|
||||||
|
|
||||||
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
|
|
||||||
String code = "auth-code";
|
|
||||||
String state = URLDecoder.decode(params.get(OAuth2ParameterNames.STATE), "UTF-8");
|
|
||||||
String redirectUri = URLDecoder.decode(params.get(OAuth2ParameterNames.REDIRECT_URI), "UTF-8");
|
|
||||||
redirectUri += "-invalid";
|
|
||||||
|
|
||||||
String authorizationResponseUri =
|
|
||||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
|
||||||
.queryParam(OAuth2ParameterNames.CODE, code)
|
|
||||||
.queryParam(OAuth2ParameterNames.STATE, state)
|
|
||||||
.build().encode().toUriString();
|
|
||||||
|
|
||||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
|
||||||
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
|
||||||
|
|
||||||
HtmlElement errorElement = page.getBody().getFirstByXPath("div");
|
|
||||||
assertThat(errorElement).isNotNull();
|
|
||||||
assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertLoginPage(HtmlPage page) throws Exception {
|
private void assertLoginPage(HtmlPage page) throws Exception {
|
||||||
assertThat(page.getTitleText()).isEqualTo("Please sign in");
|
assertThat(page.getTitleText()).isEqualTo("Please sign in");
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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"]
|
||||||
@@ -12,6 +12,7 @@ dependencies {
|
|||||||
|
|
||||||
provided 'javax.servlet:javax.servlet-api'
|
provided 'javax.servlet:javax.servlet-api'
|
||||||
|
|
||||||
|
testCompile project(path : ':spring-security-config', configuration : 'tests')
|
||||||
testCompile 'com.fasterxml.jackson.core:jackson-databind'
|
testCompile 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
testCompile 'io.projectreactor:reactor-test'
|
testCompile 'io.projectreactor:reactor-test'
|
||||||
testCompile 'javax.xml.bind:jaxb-api'
|
testCompile 'javax.xml.bind:jaxb-api'
|
||||||
|
|||||||
+1
-1
@@ -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<>();
|
||||||
|
|||||||
+87
-8
@@ -16,6 +16,11 @@
|
|||||||
package org.springframework.security.test.web.servlet.setup;
|
package org.springframework.security.test.web.servlet.setup;
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
|
||||||
import org.springframework.security.config.BeanIds;
|
import org.springframework.security.config.BeanIds;
|
||||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||||
@@ -23,6 +28,8 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
|||||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter;
|
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter;
|
||||||
import org.springframework.web.context.WebApplicationContext;
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext;
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,12 +41,13 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
|
|||||||
* @since 4.0
|
* @since 4.0
|
||||||
*/
|
*/
|
||||||
final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter {
|
final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter {
|
||||||
private Filter springSecurityFilterChain;
|
private final DelegateFilter delegateFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance
|
* Creates a new instance
|
||||||
*/
|
*/
|
||||||
SecurityMockMvcConfigurer() {
|
SecurityMockMvcConfigurer() {
|
||||||
|
this.delegateFilter = new DelegateFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,30 +55,101 @@ final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter {
|
|||||||
* @param springSecurityFilterChain the {@link javax.servlet.Filter} to use
|
* @param springSecurityFilterChain the {@link javax.servlet.Filter} to use
|
||||||
*/
|
*/
|
||||||
SecurityMockMvcConfigurer(Filter springSecurityFilterChain) {
|
SecurityMockMvcConfigurer(Filter springSecurityFilterChain) {
|
||||||
this.springSecurityFilterChain = springSecurityFilterChain;
|
this.delegateFilter = new DelegateFilter(springSecurityFilterChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> builder) {
|
||||||
|
builder.addFilters(this.delegateFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestPostProcessor beforeMockMvcCreated(
|
public RequestPostProcessor beforeMockMvcCreated(
|
||||||
ConfigurableMockMvcBuilder<?> builder, WebApplicationContext context) {
|
ConfigurableMockMvcBuilder<?> builder, WebApplicationContext context) {
|
||||||
String securityBeanId = BeanIds.SPRING_SECURITY_FILTER_CHAIN;
|
String securityBeanId = BeanIds.SPRING_SECURITY_FILTER_CHAIN;
|
||||||
if (this.springSecurityFilterChain == null
|
if (getSpringSecurityFilterChain() == null
|
||||||
&& context.containsBean(securityBeanId)) {
|
&& context.containsBean(securityBeanId)) {
|
||||||
this.springSecurityFilterChain = context.getBean(securityBeanId,
|
setSpringSecurityFitlerChain(context.getBean(securityBeanId,
|
||||||
Filter.class);
|
Filter.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.springSecurityFilterChain == null) {
|
if (getSpringSecurityFilterChain() == null) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"springSecurityFilterChain cannot be null. Ensure a Bean with the name "
|
"springSecurityFilterChain cannot be null. Ensure a Bean with the name "
|
||||||
+ securityBeanId
|
+ securityBeanId
|
||||||
+ " implementing Filter is present or inject the Filter to be used.");
|
+ " implementing Filter is present or inject the Filter to be used.");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.addFilters(this.springSecurityFilterChain);
|
// This is used by other test support to obtain the FilterChainProxy
|
||||||
context.getServletContext().setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN,
|
context.getServletContext().setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN,
|
||||||
this.springSecurityFilterChain);
|
getSpringSecurityFilterChain());
|
||||||
|
|
||||||
return testSecurityContext();
|
return testSecurityContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setSpringSecurityFitlerChain(Filter filter) {
|
||||||
|
this.delegateFilter.setDelegate(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Filter getSpringSecurityFilterChain() {
|
||||||
|
return this.delegateFilter.delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows adding in {@link #afterConfigurerAdded(ConfigurableMockMvcBuilder)} to preserve Filter order and then
|
||||||
|
* lazily set the delegate in {@link #beforeMockMvcCreated(ConfigurableMockMvcBuilder, WebApplicationContext)}.
|
||||||
|
*
|
||||||
|
* {@link org.springframework.web.filter.DelegatingFilterProxy} is not used because it is not easy to lazily set
|
||||||
|
* the delegate or get the delegate which is necessary for the test infrastructure.
|
||||||
|
*/
|
||||||
|
static class DelegateFilter implements Filter {
|
||||||
|
|
||||||
|
private Filter delegate;
|
||||||
|
|
||||||
|
DelegateFilter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateFilter(Filter delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDelegate(Filter delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Filter getDelegate() {
|
||||||
|
return this.delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig filterConfig) throws ServletException {
|
||||||
|
this.delegate.init(filterConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
this.delegate.doFilter(request, response, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
this.delegate.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.delegate.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return this.delegate.equals(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.delegate.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
|
|||||||
|
|
||||||
@RunWith(PowerMockRunner.class)
|
@RunWith(PowerMockRunner.class)
|
||||||
@PrepareOnlyThisForTest(WebTestUtils.class)
|
@PrepareOnlyThisForTest(WebTestUtils.class)
|
||||||
@PowerMockIgnore("javax.security.auth.*")
|
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
|
||||||
public class SecurityMockMvcRequestPostProcessorsAuthenticationTests {
|
public class SecurityMockMvcRequestPostProcessorsAuthenticationTests {
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<SecurityContext> contextCaptor;
|
private ArgumentCaptor<SecurityContext> contextCaptor;
|
||||||
|
|||||||
+2
@@ -32,6 +32,7 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
||||||
import org.powermock.modules.junit4.PowerMockRunner;
|
import org.powermock.modules.junit4.PowerMockRunner;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
@@ -42,6 +43,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
|
|||||||
|
|
||||||
@RunWith(PowerMockRunner.class)
|
@RunWith(PowerMockRunner.class)
|
||||||
@PrepareOnlyThisForTest(WebTestUtils.class)
|
@PrepareOnlyThisForTest(WebTestUtils.class)
|
||||||
|
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
|
||||||
public class SecurityMockMvcRequestPostProcessorsSecurityContextTests {
|
public class SecurityMockMvcRequestPostProcessorsSecurityContextTests {
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<SecurityContext> contextCaptor;
|
private ArgumentCaptor<SecurityContext> contextCaptor;
|
||||||
|
|||||||
+3
-1
@@ -29,6 +29,7 @@ import org.junit.Before;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
||||||
import org.powermock.modules.junit4.PowerMockRunner;
|
import org.powermock.modules.junit4.PowerMockRunner;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
@@ -39,6 +40,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
|
|||||||
|
|
||||||
@RunWith(PowerMockRunner.class)
|
@RunWith(PowerMockRunner.class)
|
||||||
@PrepareOnlyThisForTest(WebTestUtils.class)
|
@PrepareOnlyThisForTest(WebTestUtils.class)
|
||||||
|
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
|
||||||
public class SecurityMockMvcRequestPostProcessorsTestSecurityContextTests {
|
public class SecurityMockMvcRequestPostProcessorsTestSecurityContextTests {
|
||||||
@Mock
|
@Mock
|
||||||
private SecurityContext context;
|
private SecurityContext context;
|
||||||
@@ -81,4 +83,4 @@ public class SecurityMockMvcRequestPostProcessorsTestSecurityContextTests {
|
|||||||
spy(WebTestUtils.class);
|
spy(WebTestUtils.class);
|
||||||
when(WebTestUtils.getSecurityContextRepository(request)).thenReturn(repository);
|
when(WebTestUtils.getSecurityContextRepository(request)).thenReturn(repository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -32,6 +32,7 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
||||||
import org.powermock.modules.junit4.PowerMockRunner;
|
import org.powermock.modules.junit4.PowerMockRunner;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
@@ -44,6 +45,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
|
|||||||
|
|
||||||
@RunWith(PowerMockRunner.class)
|
@RunWith(PowerMockRunner.class)
|
||||||
@PrepareOnlyThisForTest(WebTestUtils.class)
|
@PrepareOnlyThisForTest(WebTestUtils.class)
|
||||||
|
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
|
||||||
public class SecurityMockMvcRequestPostProcessorsUserDetailsTests {
|
public class SecurityMockMvcRequestPostProcessorsUserDetailsTests {
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<SecurityContext> contextCaptor;
|
private ArgumentCaptor<SecurityContext> contextCaptor;
|
||||||
|
|||||||
+2
@@ -35,6 +35,7 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
|
||||||
import org.powermock.modules.junit4.PowerMockRunner;
|
import org.powermock.modules.junit4.PowerMockRunner;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
@@ -47,6 +48,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
|
|||||||
|
|
||||||
@RunWith(PowerMockRunner.class)
|
@RunWith(PowerMockRunner.class)
|
||||||
@PrepareOnlyThisForTest(WebTestUtils.class)
|
@PrepareOnlyThisForTest(WebTestUtils.class)
|
||||||
|
@PowerMockIgnore({"javax.security.auth.*", "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*"})
|
||||||
public class SecurityMockMvcRequestPostProcessorsUserTests {
|
public class SecurityMockMvcRequestPostProcessorsUserTests {
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<SecurityContext> contextCaptor;
|
private ArgumentCaptor<SecurityContext> contextCaptor;
|
||||||
|
|||||||
+23
-9
@@ -15,21 +15,24 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.test.web.servlet.setup;
|
package org.springframework.security.test.web.servlet.setup;
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
|
||||||
import javax.servlet.ServletContext;
|
|
||||||
|
|
||||||
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;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
import org.springframework.security.config.BeanIds;
|
import org.springframework.security.config.BeanIds;
|
||||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||||
import org.springframework.web.context.WebApplicationContext;
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
import static org.mockito.Matchers.anyString;
|
import javax.servlet.Filter;
|
||||||
import static org.mockito.Matchers.eq;
|
import javax.servlet.ServletContext;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -56,9 +59,10 @@ public class SecurityMockMvcConfigurerTests {
|
|||||||
returnFilterBean();
|
returnFilterBean();
|
||||||
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer(this.filter);
|
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer(this.filter);
|
||||||
|
|
||||||
|
configurer.afterConfigurerAdded(this.builder);
|
||||||
configurer.beforeMockMvcCreated(this.builder, this.context);
|
configurer.beforeMockMvcCreated(this.builder, this.context);
|
||||||
|
|
||||||
verify(this.builder).addFilters(this.filter);
|
assertFilterAdded(this.filter);
|
||||||
verify(this.servletContext).setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN,
|
verify(this.servletContext).setAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN,
|
||||||
this.filter);
|
this.filter);
|
||||||
}
|
}
|
||||||
@@ -68,27 +72,37 @@ public class SecurityMockMvcConfigurerTests {
|
|||||||
returnFilterBean();
|
returnFilterBean();
|
||||||
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer();
|
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer();
|
||||||
|
|
||||||
|
configurer.afterConfigurerAdded(this.builder);
|
||||||
configurer.beforeMockMvcCreated(this.builder, this.context);
|
configurer.beforeMockMvcCreated(this.builder, this.context);
|
||||||
|
|
||||||
verify(this.builder).addFilters(this.beanFilter);
|
assertFilterAdded(this.beanFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void beforeMockMvcCreatedNoBean() throws Exception {
|
public void beforeMockMvcCreatedNoBean() throws Exception {
|
||||||
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer(this.filter);
|
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer(this.filter);
|
||||||
|
|
||||||
|
configurer.afterConfigurerAdded(this.builder);
|
||||||
configurer.beforeMockMvcCreated(this.builder, this.context);
|
configurer.beforeMockMvcCreated(this.builder, this.context);
|
||||||
|
|
||||||
verify(this.builder).addFilters(this.filter);
|
assertFilterAdded(this.filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void beforeMockMvcCreatedNoFilter() throws Exception {
|
public void beforeMockMvcCreatedNoFilter() throws Exception {
|
||||||
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer();
|
SecurityMockMvcConfigurer configurer = new SecurityMockMvcConfigurer();
|
||||||
|
|
||||||
|
configurer.afterConfigurerAdded(this.builder);
|
||||||
configurer.beforeMockMvcCreated(this.builder, this.context);
|
configurer.beforeMockMvcCreated(this.builder, this.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertFilterAdded(Filter filter) throws IOException, ServletException {
|
||||||
|
ArgumentCaptor<SecurityMockMvcConfigurer.DelegateFilter> filterArg = ArgumentCaptor.forClass(
|
||||||
|
SecurityMockMvcConfigurer.DelegateFilter.class);
|
||||||
|
verify(this.builder).addFilters(filterArg.capture());
|
||||||
|
assertThat(filterArg.getValue().getDelegate()).isEqualTo(filter);
|
||||||
|
}
|
||||||
|
|
||||||
private void returnFilterBean() {
|
private void returnFilterBean() {
|
||||||
when(this.context.containsBean(anyString())).thenReturn(true);
|
when(this.context.containsBean(anyString())).thenReturn(true);
|
||||||
when(this.context.getBean(anyString(), eq(Filter.class)))
|
when(this.context.getBean(anyString(), eq(Filter.class)))
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.test.web.servlet.setup;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.users.AuthenticationTestConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.springframework.test.context.web.WebAppConfiguration;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
|
import javax.servlet.Filter;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@WebAppConfiguration
|
||||||
|
public class SecurityMockMvcConfigurersTests {
|
||||||
|
@Autowired
|
||||||
|
WebApplicationContext wac;
|
||||||
|
|
||||||
|
Filter noOpFilter = mock(Filter.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since noOpFilter is first does not continue the chain, security will not be invoked and the status should be OK
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void applySpringSecurityWhenAddFilterFirstThenFilterFirst() throws Exception {
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
|
||||||
|
.addFilters(this.noOpFilter)
|
||||||
|
.apply(springSecurity())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/"))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since noOpFilter is second security will be invoked and the status will be not OK. We know this because if noOpFilter
|
||||||
|
* were first security would not be invoked sincet noOpFilter does not continue the FilterChain
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void applySpringSecurityWhenAddFilterSecondThenSecurityFirst() throws Exception {
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
|
||||||
|
.apply(springSecurity())
|
||||||
|
.addFilters(this.noOpFilter)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/"))
|
||||||
|
.andExpect(status().is4xxClientError());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebMvc
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Import(AuthenticationTestConfiguration.class)
|
||||||
|
static class Config {}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
* <bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
|
* <bean id="myfilterChainProxy" class="org.springframework.security.web.FilterChainProxy">
|
||||||
* <constructor-arg>
|
* <constructor-arg>
|
||||||
* <util:list>
|
* <util:list>
|
||||||
* <security:filter-chain pattern="/do/not/filter*" filters="none"/>
|
* <security:filter-chain pattern="/do/not/filter*" filters="none"/>
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-14
@@ -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.
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.web.authentication.session;
|
package org.springframework.security.web.authentication.session;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -45,8 +46,9 @@ import org.springframework.util.Assert;
|
|||||||
* </p>
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* If a user has reached the maximum number of permitted sessions, the behaviour depends
|
* If a user has reached the maximum number of permitted sessions, the behaviour depends
|
||||||
* on the <tt>exceptionIfMaxExceeded</tt> property. The default behaviour is to expired
|
* on the <tt>exceptionIfMaxExceeded</tt> property. The default behaviour is to expire
|
||||||
* the least recently used session, which will be invalidated by the
|
* any sessions that exceed the maximum number of permitted sessions, starting with the
|
||||||
|
* least recently used sessions. The expired sessions will be invalidated by the
|
||||||
* {@link ConcurrentSessionFilter} if accessed again. If <tt>exceptionIfMaxExceeded</tt>
|
* {@link ConcurrentSessionFilter} if accessed again. If <tt>exceptionIfMaxExceeded</tt>
|
||||||
* is set to <tt>true</tt>, however, the user will be prevented from starting a new
|
* is set to <tt>true</tt>, however, the user will be prevented from starting a new
|
||||||
* authenticated session.
|
* authenticated session.
|
||||||
@@ -156,18 +158,13 @@ public class ConcurrentSessionControlAuthenticationStrategy implements
|
|||||||
"Maximum sessions of {0} for this principal exceeded"));
|
"Maximum sessions of {0} for this principal exceeded"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine least recently used session, and mark it for invalidation
|
// Determine least recently used sessions, and mark them for invalidation
|
||||||
SessionInformation leastRecentlyUsed = null;
|
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
|
||||||
|
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
|
||||||
for (SessionInformation session : sessions) {
|
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
|
||||||
if ((leastRecentlyUsed == null)
|
for (SessionInformation session: sessionsToBeExpired) {
|
||||||
|| session.getLastRequest()
|
session.expireNow();
|
||||||
.before(leastRecentlyUsed.getLastRequest())) {
|
|
||||||
leastRecentlyUsed = session;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leastRecentlyUsed.expireNow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-5
@@ -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);
|
||||||
|
|||||||
+4
-4
@@ -19,12 +19,11 @@ package org.springframework.security.web.server.authentication;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegates to a collection of {@link ServerAuthenticationSuccessHandler} implementations.
|
* Delegates to a collection of {@link ServerAuthenticationSuccessHandler} implementations.
|
||||||
@@ -43,7 +42,8 @@ public class DelegatingServerAuthenticationSuccessHandler implements ServerAuthe
|
|||||||
@Override
|
@Override
|
||||||
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange,
|
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
Stream<Mono<Void>> results = this.delegates.stream().map(delegate -> delegate.onAuthenticationSuccess(exchange, authentication));
|
return Flux.fromIterable(this.delegates)
|
||||||
return Mono.when(results.collect(Collectors.toList()));
|
.concatMap(delegate -> delegate.onAuthenticationSuccess(exchange, authentication))
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-7
@@ -20,9 +20,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -50,10 +49,8 @@ public class DelegatingServerLogoutHandler implements ServerLogoutHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
||||||
return Mono.when(this.delegates.stream()
|
return Flux.fromIterable(this.delegates)
|
||||||
.filter(Objects::nonNull)
|
.concatMap(delegate -> delegate.logout(exchange, authentication))
|
||||||
.map(delegate -> delegate.logout(exchange, authentication))
|
.then();
|
||||||
.collect(Collectors.toList())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
+7
-8
@@ -15,14 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.web.server.header;
|
package org.springframework.security.web.server.header;
|
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
|
||||||
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines multiple {@link ServerHttpHeadersWriter} instances into a single instance.
|
* Combines multiple {@link ServerHttpHeadersWriter} instances into a single instance.
|
||||||
@@ -43,8 +41,9 @@ public class CompositeServerHttpHeadersWriter implements ServerHttpHeadersWriter
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
|
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
|
||||||
Stream<Mono<Void>> results = writers.stream().map( writer -> writer.writeHttpHeaders(exchange));
|
return Flux.fromIterable(this.writers)
|
||||||
return Mono.when(results.collect(Collectors.toList()));
|
.concatMap(w -> w.writeHttpHeaders(exchange))
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -106,7 +106,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
|||||||
+ " <body>\n"
|
+ " <body>\n"
|
||||||
+ " <div class=\"container\">\n"
|
+ " <div class=\"container\">\n"
|
||||||
+ formLogin(queryParams, csrfTokenHtmlInput)
|
+ formLogin(queryParams, csrfTokenHtmlInput)
|
||||||
+ oauth2LoginLinks(contextPath, this.oauth2AuthenticationUrlToClientName)
|
+ oauth2LoginLinks(queryParams, contextPath, this.oauth2AuthenticationUrlToClientName)
|
||||||
+ " </div>\n"
|
+ " </div>\n"
|
||||||
+ " </body>\n"
|
+ " </body>\n"
|
||||||
+ "</html>";
|
+ "</html>";
|
||||||
@@ -135,12 +135,14 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
|||||||
+ " </form>\n";
|
+ " </form>\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String oauth2LoginLinks(String contextPath, Map<String, String> oauth2AuthenticationUrlToClientName) {
|
private static String oauth2LoginLinks(MultiValueMap<String, String> queryParams, String contextPath, Map<String, String> oauth2AuthenticationUrlToClientName) {
|
||||||
if (oauth2AuthenticationUrlToClientName.isEmpty()) {
|
if (oauth2AuthenticationUrlToClientName.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
boolean isError = queryParams.containsKey("error");
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("<div class=\"container\"><h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
|
sb.append("<div class=\"container\"><h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
|
||||||
|
sb.append(createError(isError));
|
||||||
sb.append("<table class=\"table table-striped\">\n");
|
sb.append("<table class=\"table table-striped\">\n");
|
||||||
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
|
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
|
||||||
sb.append(" <tr><td>");
|
sb.append(" <tr><td>");
|
||||||
|
|||||||
+7
-6
@@ -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() {
|
||||||
|
|||||||
+6
@@ -69,6 +69,12 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
|
|||||||
super.setContentLength(len);
|
super.setContentLength(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentLengthLong(long len) {
|
||||||
|
setContentLength(len);
|
||||||
|
super.setContentLengthLong(len);
|
||||||
|
}
|
||||||
|
|
||||||
private void setContentLength(long len) {
|
private void setContentLength(long len) {
|
||||||
this.contentLength = len;
|
this.contentLength = len;
|
||||||
checkContentLength(0);
|
checkContentLength(0);
|
||||||
|
|||||||
+3
-3
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
+33
-5
@@ -16,10 +16,6 @@
|
|||||||
|
|
||||||
package org.springframework.security.web.server.authentication;
|
package org.springframework.security.web.server.authentication;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@@ -27,9 +23,19 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.publisher.PublisherProbe;
|
import reactor.test.publisher.PublisherProbe;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
@@ -88,4 +94,26 @@ public class DelegatingServerAuthenticationSuccessHandlerTests {
|
|||||||
this.delegate1Result.assertWasSubscribed();
|
this.delegate1Result.assertWasSubscribed();
|
||||||
this.delegate2Result.assertWasSubscribed();
|
this.delegate2Result.assertWasSubscribed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onAuthenticationSuccessSequential() throws Exception {
|
||||||
|
AtomicBoolean slowDone = new AtomicBoolean();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
ServerAuthenticationSuccessHandler slow = (exchange, authentication) ->
|
||||||
|
Mono.delay(Duration.ofMillis(100))
|
||||||
|
.doOnSuccess(__ -> slowDone.set(true))
|
||||||
|
.then();
|
||||||
|
ServerAuthenticationSuccessHandler second = (exchange, authentication) ->
|
||||||
|
Mono.fromRunnable(() -> {
|
||||||
|
latch.countDown();
|
||||||
|
assertThat(slowDone.get())
|
||||||
|
.describedAs("ServerAuthenticationSuccessHandler should be executed sequentially")
|
||||||
|
.isTrue();
|
||||||
|
});
|
||||||
|
DelegatingServerAuthenticationSuccessHandler handler = new DelegatingServerAuthenticationSuccessHandler(slow, second);
|
||||||
|
|
||||||
|
handler.onAuthenticationSuccess(this.exchange, this.authentication).block();
|
||||||
|
|
||||||
|
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2013 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* 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.
|
||||||
@@ -134,6 +134,25 @@ public class ConcurrentSessionControlAuthenticationStrategyTests {
|
|||||||
assertThat(sessionInformation.isExpired()).isTrue();
|
assertThat(sessionInformation.isExpired()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpired() {
|
||||||
|
SessionInformation oldestSessionInfo = new SessionInformation(
|
||||||
|
authentication.getPrincipal(), "unique1", new Date(1374766134214L));
|
||||||
|
SessionInformation secondOldestSessionInfo = new SessionInformation(
|
||||||
|
authentication.getPrincipal(), "unique2", new Date(1374766134215L));
|
||||||
|
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(
|
||||||
|
Arrays.<SessionInformation> asList(oldestSessionInfo,
|
||||||
|
secondOldestSessionInfo,
|
||||||
|
sessionInformation));
|
||||||
|
strategy.setMaximumSessions(2);
|
||||||
|
|
||||||
|
strategy.onAuthentication(authentication, request, response);
|
||||||
|
|
||||||
|
assertThat(oldestSessionInfo.isExpired()).isTrue();
|
||||||
|
assertThat(secondOldestSessionInfo.isExpired()).isTrue();
|
||||||
|
assertThat(sessionInformation.isExpired()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void setMessageSourceNull() {
|
public void setMessageSourceNull() {
|
||||||
strategy.setMessageSource(null);
|
strategy.setMessageSource(null);
|
||||||
|
|||||||
+42
-5
@@ -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();
|
||||||
|
|||||||
+18
-1
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package org.springframework.security.web.server.authentication.logout;
|
package org.springframework.security.web.server.authentication.logout;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@@ -28,9 +29,14 @@ import org.mockito.junit.MockitoJUnitRunner;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.publisher.PublisherProbe;
|
import reactor.test.publisher.PublisherProbe;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Eric Deandrea
|
* @author Eric Deandrea
|
||||||
@@ -98,4 +104,26 @@ public class DelegatingServerLogoutHandlerTests {
|
|||||||
this.delegate1Result.assertWasSubscribed();
|
this.delegate1Result.assertWasSubscribed();
|
||||||
this.delegate2Result.assertWasSubscribed();
|
this.delegate2Result.assertWasSubscribed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void logoutSequential() throws Exception {
|
||||||
|
AtomicBoolean slowDone = new AtomicBoolean();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
ServerLogoutHandler slow = (exchange, authentication) ->
|
||||||
|
Mono.delay(Duration.ofMillis(100))
|
||||||
|
.doOnSuccess(__ -> slowDone.set(true))
|
||||||
|
.then();
|
||||||
|
ServerLogoutHandler second = (exchange, authentication) ->
|
||||||
|
Mono.fromRunnable(() -> {
|
||||||
|
latch.countDown();
|
||||||
|
assertThat(slowDone.get())
|
||||||
|
.describedAs("ServerLogoutHandler should be executed sequentially")
|
||||||
|
.isTrue();
|
||||||
|
});
|
||||||
|
DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(slow, second);
|
||||||
|
|
||||||
|
handler.logout(this.exchange, this.authentication).block();
|
||||||
|
|
||||||
|
assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package org.springframework.security.web.server.csrf;
|
package org.springframework.security.web.server.csrf;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -28,6 +26,8 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
|||||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Eric Deandrea
|
* @author Eric Deandrea
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
@@ -111,7 +111,7 @@ public class CookieServerCsrfTokenRepositoryTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void saveTokenWhenCustomPropertiesThenCustomProperties() {
|
public void saveTokenWhenCustomPropertiesThenCustomProperties() {
|
||||||
setExpectedDomain(".spring.io");
|
setExpectedDomain("spring.io");
|
||||||
setExpectedCookieName("csrfCookie");
|
setExpectedCookieName("csrfCookie");
|
||||||
setExpectedPath("/some/path");
|
setExpectedPath("/some/path");
|
||||||
setExpectedHeaderName("headerName");
|
setExpectedHeaderName("headerName");
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user