Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52814ab387 | |||
| 9c5ce287d7 | |||
| cb055f1402 | |||
| 8b3777d0f1 | |||
| 7181f576fc | |||
| dba3dacc19 | |||
| 5d0eb32f4d | |||
| 984fdd4c00 | |||
| 58b68625bb | |||
| 6a87ccca4a | |||
| 925a6685d9 | |||
| bd3e3ed2c5 | |||
| 6dad918e7b | |||
| 89ad42a06f | |||
| 85889d5e0b | |||
| 8c7dbf2e3d | |||
| 2443675a59 | |||
| 04d6d5ae74 | |||
| acbbb6941c | |||
| 4a1b96d51a | |||
| 9fbed06b6b | |||
| fea90f9328 | |||
| 22a1bce83c | |||
| 8dfe0fa815 | |||
| b408f074d5 | |||
| 669fdba98c | |||
| b4d2217d45 | |||
| b1e09eddaa | |||
| b425b622db | |||
| 6512c04079 | |||
| da4bd22c6d | |||
| fd669f751d | |||
| 510d1b8121 | |||
| 2f80b8a5be | |||
| 0efdb2c92c | |||
| 79d8b616f0 | |||
| 9d8920f1b1 | |||
| 6519029340 | |||
| 3dc59686fa | |||
| dcd2137418 | |||
| 1bb49bbc2e | |||
| 5f395a3513 | |||
| 0356af03e6 | |||
| 5343325091 | |||
| fa9898dd6d | |||
| bff6d82dd0 | |||
| 674e2c0a8e | |||
| 11c1236261 | |||
| 98467755ad | |||
| 0e37c722e2 | |||
| c71a893e08 | |||
| 69ff2ab3fc | |||
| 38a731d5e4 | |||
| d5eeec0ae6 | |||
| fd7fe482f5 | |||
| b88e094f37 | |||
| 04453cec96 | |||
| d9919b838d | |||
| 05b9591e44 | |||
| 1b93b0c9a6 | |||
| e87783024c | |||
| 7e9c7534f5 | |||
| 24fd9579c5 | |||
| a6dd119266 | |||
| 774ea6980b | |||
| 279ddbe223 | |||
| eaaee899fc | |||
| 21c1d98f64 | |||
| 9f33ce312a | |||
| e382c269ef | |||
| ce7c501f9c | |||
| bd93616567 | |||
| f9872d3dee | |||
| b7212bd975 | |||
| 50da82d88d | |||
| 3ed31400f7 | |||
| 69b1bc62ff | |||
| f43d00fac6 | |||
| 01cce49ee5 | |||
| 6ccc18fde2 | |||
| d34f7a8474 | |||
| ba78fae246 | |||
| 98922b001a | |||
| 19a4386e49 | |||
| 4fa67a1d9b | |||
| 94fd7e3a2b | |||
| e9e736d1e6 | |||
| c399185365 | |||
| 93a1fc104c | |||
| 9dd68f86d3 | |||
| 01f904895e | |||
| 067cb4579e | |||
| 7b34b223e6 | |||
| 52ed597f4c | |||
| 564d5644c9 | |||
| f011c36ba4 | |||
| bb654fdcdf | |||
| 30a63026f8 |
@@ -0,0 +1,22 @@
|
||||
name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '8'
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
language: java
|
||||
|
||||
jdk:
|
||||
- openjdk8
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
|
||||
script: ./gradlew build --refresh-dependencies --no-daemon --continue
|
||||
Vendored
+1
-1
@@ -56,7 +56,7 @@ try {
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
|
||||
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.2.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: snapshots'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
|
||||
|
||||
image:https://travis-ci.org/spring-projects/spring-security.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/spring-security"]
|
||||
|
||||
= Spring Security
|
||||
|
||||
Spring Security provides security services for the https://docs.spring.io[Spring IO Platform]. Spring Security 5.0 requires Spring 5.0 as
|
||||
|
||||
+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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -131,10 +131,9 @@ public class AccessControlEntryImpl implements AccessControlEntry,
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.acl.hashCode();
|
||||
result = 31 * result + this.permission.hashCode();
|
||||
int result = this.permission.hashCode();
|
||||
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.auditSuccess ? 1 : 0);
|
||||
result = 31 * result + (this.granting ? 1 : 0);
|
||||
|
||||
@@ -118,7 +118,7 @@ class AclClassIdUtils {
|
||||
*/
|
||||
private Long convertToLong(Serializable identifier) {
|
||||
Long idAsLong;
|
||||
if (canConvertFromStringTo(Long.class)) {
|
||||
if (conversionService.canConvert(identifier.getClass(), Long.class)) {
|
||||
idAsLong = conversionService.convert(identifier, Long.class);
|
||||
} else {
|
||||
idAsLong = Long.valueOf(identifier.toString());
|
||||
|
||||
@@ -577,6 +577,25 @@ public class AclImplTests {
|
||||
assertThat(acl.isGranted(permissions, sids, false)).isTrue();
|
||||
}
|
||||
|
||||
@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
|
||||
// ==================================================================================================
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
@@ -39,6 +40,7 @@ import static org.mockito.BDDMockito.given;
|
||||
public class AclClassIdUtilsTest {
|
||||
|
||||
private static final Long DEFAULT_IDENTIFIER = 999L;
|
||||
private static final BigInteger BIGINT_IDENTIFIER = new BigInteger("999");
|
||||
private static final String DEFAULT_IDENTIFIER_AS_STRING = DEFAULT_IDENTIFIER.toString();
|
||||
|
||||
@Mock
|
||||
@@ -62,6 +64,15 @@ public class AclClassIdUtilsTest {
|
||||
assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnLongIfIdentifierIsBigInteger() throws SQLException {
|
||||
// when
|
||||
Serializable newIdentifier = aclClassIdUtils.identifierFrom(BIGINT_IDENTIFIER, resultSet);
|
||||
|
||||
// then
|
||||
assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnLongIfClassIdTypeIsNull() throws SQLException {
|
||||
// given
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.23.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.2.RELEASE'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
|
||||
classpath "io.freefair.gradle:aspectj-plugin:4.0.2"
|
||||
}
|
||||
repositories {
|
||||
|
||||
+4
@@ -16,8 +16,10 @@
|
||||
package org.springframework.security.config.annotation.configuration;
|
||||
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
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
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class ObjectPostProcessorConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public ObjectPostProcessor<Object> objectPostProcessor(
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
return new AutowireBeanFactoryObjectPostProcessor(beanFactory);
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.*;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
@@ -80,6 +81,7 @@ import org.springframework.util.Assert;
|
||||
* @see EnableGlobalMethodSecurity
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class GlobalMethodSecurityConfiguration
|
||||
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware {
|
||||
private static final Log logger = LogFactory
|
||||
|
||||
+4
@@ -15,14 +15,18 @@
|
||||
*/
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
class Jsr250MetadataSourceConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() {
|
||||
return new Jsr250MethodSecurityMetadataSource();
|
||||
}
|
||||
|
||||
+2
@@ -54,6 +54,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public DelegatingMethodSecurityMetadataSource methodMetadataSource(MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
|
||||
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
|
||||
methodSecurityExpressionHandler);
|
||||
@@ -74,6 +75,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
|
||||
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
||||
if (this.grantedAuthorityDefaults != null) {
|
||||
|
||||
+1
@@ -1105,6 +1105,7 @@ public final class HttpSecurity extends
|
||||
|
||||
/**
|
||||
* Allows restricting access based upon the {@link HttpServletRequest} using
|
||||
* {@link RequestMatcher} implementations (i.e. via URL patterns).
|
||||
*
|
||||
* <h2>Example Configurations</h2>
|
||||
*
|
||||
|
||||
+8
-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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,6 +31,7 @@ import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.SecurityBuilder;
|
||||
@@ -74,6 +75,7 @@ import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
* @see WebSecurityConfiguration
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Evgeniy Cheban
|
||||
* @since 3.2
|
||||
*/
|
||||
public final class WebSecurity extends
|
||||
@@ -383,6 +385,11 @@ public final class WebSecurity extends
|
||||
throws BeansException {
|
||||
this.defaultWebSecurityExpressionHandler
|
||||
.setApplicationContext(applicationContext);
|
||||
|
||||
try {
|
||||
this.defaultWebSecurityExpressionHandler.setRoleHierarchy(applicationContext.getBean(RoleHierarchy.class));
|
||||
} catch (NoSuchBeanDefinitionException e) {}
|
||||
|
||||
try {
|
||||
this.defaultWebSecurityExpressionHandler.setPermissionEvaluator(applicationContext.getBean(
|
||||
PermissionEvaluator.class));
|
||||
|
||||
+11
@@ -330,6 +330,13 @@ public abstract class WebSecurityConfigurerAdapter implements
|
||||
/**
|
||||
* Override this method to configure {@link WebSecurity}. For example, if you wish to
|
||||
* ignore certain requests.
|
||||
*
|
||||
* Endpoints specified in this method will be ignored by Spring Security, meaning it
|
||||
* will not protect them from CSRF, XSS, Clickjacking, and so on.
|
||||
*
|
||||
* Instead, if you want to protect endpoints against common vulnerabilities, then see
|
||||
* {@link #configure(HttpSecurity)} and the {@link HttpSecurity#authorizeRequests}
|
||||
* configuration method.
|
||||
*/
|
||||
public void configure(WebSecurity web) throws Exception {
|
||||
}
|
||||
@@ -343,6 +350,10 @@ public abstract class WebSecurityConfigurerAdapter implements
|
||||
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
|
||||
* </pre>
|
||||
*
|
||||
* Any endpoint that requires defense against common vulnerabilities can be specified here, including public ones.
|
||||
* See {@link HttpSecurity#authorizeRequests} and the `permitAll()` authorization rule
|
||||
* for more details on public endpoints.
|
||||
*
|
||||
* @param http the {@link HttpSecurity} to modify
|
||||
* @throws Exception if an error occurs
|
||||
*/
|
||||
|
||||
+2
-2
@@ -183,7 +183,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
*
|
||||
* <p>
|
||||
* Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
|
||||
* "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* "https://web.archive.org/web/20160201174302/https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* >X-XSS-Protection header</a>
|
||||
* </p>
|
||||
*
|
||||
@@ -198,7 +198,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
*
|
||||
* <p>
|
||||
* Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
|
||||
* "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* "https://web.archive.org/web/20160201174302/https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* >X-XSS-Protection header</a>
|
||||
* </p>
|
||||
*
|
||||
|
||||
+3
-2
@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@@ -87,11 +88,11 @@ class ServerHttpSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
|
||||
AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver) {
|
||||
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) {
|
||||
return new WebFluxConfigurer() {
|
||||
@Override
|
||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||
configurer.addCustomResolver(authenticationPrincipalArgumentResolver);
|
||||
configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+15
-4
@@ -33,6 +33,8 @@ import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
@@ -1089,8 +1091,12 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationConverter getAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||
if (this.authenticationConverter == null) {
|
||||
ServerOAuth2AuthorizationCodeAuthenticationTokenConverter authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
||||
authenticationConverter.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
|
||||
ServerOAuth2AuthorizationCodeAuthenticationTokenConverter delegate =
|
||||
new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
||||
delegate.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
|
||||
ServerAuthenticationConverter authenticationConverter = exchange ->
|
||||
delegate.convert(exchange).onErrorMap(OAuth2AuthorizationException.class,
|
||||
e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()));
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
return this.authenticationConverter;
|
||||
@@ -2649,7 +2655,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public AuthorizeExchangeSpec hasAuthority(String authority) {
|
||||
@@ -3727,7 +3733,8 @@ public class ServerHttpSecurity {
|
||||
*/
|
||||
public final class LogoutSpec {
|
||||
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}
|
||||
@@ -3791,6 +3798,10 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
private ServerLogoutHandler createLogoutHandler() {
|
||||
ServerSecurityContextRepository securityContextRepository = ServerHttpSecurity.this.securityContextRepository;
|
||||
if (securityContextRepository != null) {
|
||||
this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository);
|
||||
}
|
||||
if (this.logoutHandlers.isEmpty()) {
|
||||
return null;
|
||||
} else if (this.logoutHandlers.size() == 1) {
|
||||
|
||||
+29
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +32,8 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
|
||||
@@ -69,6 +71,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Joe Grandja
|
||||
* @author Evgeniy Cheban
|
||||
*/
|
||||
public class WebSecurityConfigurationTests {
|
||||
@Rule
|
||||
@@ -290,6 +293,31 @@ public class WebSecurityConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityExpressionHandlerWhenRoleHierarchyBeanThenRoleHierarchyUsed() {
|
||||
this.spring.register(WebSecurityExpressionHandlerRoleHierarchyBeanConfig.class).autowire();
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "notused", "ROLE_ADMIN");
|
||||
FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest("GET", ""),
|
||||
new MockHttpServletResponse(), new MockFilterChain());
|
||||
|
||||
AbstractSecurityExpressionHandler handler = this.spring.getContext().getBean(AbstractSecurityExpressionHandler.class);
|
||||
EvaluationContext evaluationContext = handler.createEvaluationContext(authentication, invocation);
|
||||
Expression expression = handler.getExpressionParser()
|
||||
.parseExpression("hasRole('ROLE_USER')");
|
||||
boolean granted = expression.getValue(evaluationContext, Boolean.class);
|
||||
assertThat(granted).isTrue();
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class WebSecurityExpressionHandlerRoleHierarchyBeanConfig extends WebSecurityConfigurerAdapter {
|
||||
@Bean
|
||||
RoleHierarchy roleHierarchy() {
|
||||
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
|
||||
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
|
||||
return roleHierarchy;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEvaluatorUsed() {
|
||||
this.spring.register(WebSecurityExpressionHandlerPermissionEvaluatorBeanConfig.class).autowire();
|
||||
|
||||
+21
@@ -47,6 +47,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
|
||||
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.result.view.AbstractView;
|
||||
@@ -434,4 +436,23 @@ public class EnableWebFluxSecurityTests {
|
||||
Child() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-8596
|
||||
public void resolveAuthenticationPrincipalArgumentResolverFirstDoesNotCauseBeanCurrentlyInCreationException() {
|
||||
this.spring.register(EnableWebFluxSecurityConfiguration.class,
|
||||
ReactiveAuthenticationTestConfiguration.class,
|
||||
DelegatingWebFluxConfiguration.class).autowire();
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class EnableWebFluxSecurityConfiguration {
|
||||
/**
|
||||
* It is necessary to Autowire AuthenticationPrincipalArgumentResolver because it triggers eager loading of
|
||||
* AuthenticationPrincipalArgumentResolver bean which causes BeanCurrentlyInCreationException
|
||||
*/
|
||||
@Autowired
|
||||
AuthenticationPrincipalArgumentResolver resolver;
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -21,6 +21,7 @@ import org.openqa.selenium.WebDriver;
|
||||
import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
|
||||
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
||||
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.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
@@ -200,4 +201,46 @@ public class LogoutSpecTests {
|
||||
homePage
|
||||
.assertAt();
|
||||
}
|
||||
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -83,6 +83,7 @@ import org.springframework.security.web.server.authentication.RedirectServerAuth
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
@@ -102,7 +103,10 @@ import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
|
||||
|
||||
/**
|
||||
@@ -682,7 +686,6 @@ public class OAuth2LoginTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void logoutWhenUsingOidcLogoutHandlerThenRedirects() {
|
||||
this.spring.register(OAuth2LoginConfigWithOidcLogoutSuccessHandler.class).autowire();
|
||||
@@ -716,6 +719,8 @@ public class OAuth2LoginTests {
|
||||
http
|
||||
.csrf().disable()
|
||||
.logout()
|
||||
// avoid using mock ServerSecurityContextRepository for logout
|
||||
.logoutHandler(new SecurityContextServerLogoutHandler())
|
||||
.logoutSuccessHandler(
|
||||
new OidcClientInitiatedServerLogoutSuccessHandler(
|
||||
new InMemoryReactiveClientRegistrationRepository(this.withLogout)))
|
||||
@@ -736,6 +741,24 @@ public class OAuth2LoginTests {
|
||||
}
|
||||
}
|
||||
|
||||
// gh-8609
|
||||
@Test
|
||||
public void oauth2LoginWhenAuthenticationConverterFailsThenDefaultRedirectToLogin() {
|
||||
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class).autowire();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(this.springSecurity)
|
||||
.build();
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/login/oauth2/code/google")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.valueEquals("Location", "/login?error");
|
||||
}
|
||||
|
||||
static class GitHubWebFilter implements WebFilter {
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager implement
|
||||
|
||||
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
|
||||
|
||||
private Scheduler scheduler = Schedulers.newParallel("password-encoder", Schedulers.DEFAULT_POOL_SIZE, true);
|
||||
private Scheduler scheduler = Schedulers.boundedElastic();
|
||||
|
||||
private UserDetailsChecker preAuthenticationChecks = user -> {
|
||||
if (!user.isAccountNonLocked()) {
|
||||
|
||||
@@ -213,7 +213,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
((CredentialsContainer) result).eraseCredentials();
|
||||
}
|
||||
|
||||
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
|
||||
// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
|
||||
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
|
||||
if (parentResult == null) {
|
||||
eventPublisher.publishAuthenticationSuccess(result);
|
||||
@@ -230,7 +230,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
"No AuthenticationProvider found for {0}"));
|
||||
}
|
||||
|
||||
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
|
||||
// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
|
||||
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
|
||||
if (parentException == null) {
|
||||
prepareException(lastException, authentication);
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.SpringVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
@@ -108,8 +109,9 @@ public class SpringSecurityCoreVersion {
|
||||
*/
|
||||
private static String getSpringVersion() {
|
||||
Properties properties = new Properties();
|
||||
try {
|
||||
properties.load(SpringSecurityCoreVersion.class.getClassLoader().getResourceAsStream("META-INF/spring-security.versions"));
|
||||
try (InputStream is = SpringSecurityCoreVersion.class.getClassLoader()
|
||||
.getResourceAsStream("META-INF/spring-security.versions")) {
|
||||
properties.load(is);
|
||||
} catch (IOException | NullPointerException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found
|
||||
JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority
|
||||
JdbcDaoImpl.notFound=User {0} not found
|
||||
LdapAuthenticationProvider.badCredentials=Bad credentials
|
||||
LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
|
||||
LdapAuthenticationProvider.credentialsExpired=User credentials have expired
|
||||
LdapAuthenticationProvider.disabled=User is disabled
|
||||
LdapAuthenticationProvider.expired=User account has expired
|
||||
|
||||
+8
@@ -99,6 +99,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
}
|
||||
|
||||
public String encode(CharSequence rawPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
|
||||
String salt;
|
||||
if (random != null) {
|
||||
salt = BCrypt.gensalt(version.getVersion(), strength, random);
|
||||
@@ -109,6 +113,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
}
|
||||
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
|
||||
if (encodedPassword == null || encodedPassword.length() == 0) {
|
||||
logger.warn("Empty encoded password");
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,7 +32,7 @@ public class Encryptors {
|
||||
* (Password-Based Key Derivation 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 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.
|
||||
*
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
@@ -50,7 +50,7 @@ public class Encryptors {
|
||||
* 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
|
||||
* 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.
|
||||
* NOTE: This mode is not
|
||||
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
|
||||
@@ -63,7 +63,7 @@ public class Encryptors {
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #stronger(CharSequence, CharSequence) which uses the significatly more secure
|
||||
* @see #stronger(CharSequence, CharSequence), which uses the significatly more secure
|
||||
* GCM (instead of CBC)
|
||||
*/
|
||||
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
||||
@@ -105,7 +105,10 @@ public class Encryptors {
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* 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) {
|
||||
return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
|
||||
salt));
|
||||
|
||||
-2
@@ -83,8 +83,6 @@ public class Md4PasswordEncoder implements PasswordEncoder {
|
||||
private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
|
||||
private boolean encodeHashAsBase64;
|
||||
|
||||
private Digester digester;
|
||||
|
||||
|
||||
public void setEncodeHashAsBase64(boolean 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
|
||||
* adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
|
||||
* 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
|
||||
public final class NoOpPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
+12
@@ -200,4 +200,16 @@ public class BCryptPasswordEncoderTests {
|
||||
encoder.upgradeEncoding("not-a-bcrypt-password");
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ asciidoctor {
|
||||
'gh-samples-url': "$ghUrl/samples"
|
||||
}
|
||||
|
||||
remotes {
|
||||
docs {
|
||||
retryCount = 5 // retry 5 times (default is 0)
|
||||
retryWaitSec = 10 // wait 10 seconds between retries (default is 0)
|
||||
}
|
||||
}
|
||||
|
||||
docsZip {
|
||||
from(project(':spring-security-docs-guides').asciidoctor) {
|
||||
into 'guides'
|
||||
|
||||
@@ -15,7 +15,7 @@ It maps the certificate to an application user and loads that user's set of gran
|
||||
|
||||
You should be familiar with using certificates and setting up client authentication for your servlet container before attempting to use it with Spring Security.
|
||||
Most of the work is in creating and installing suitable certificates and keys.
|
||||
For example, if you're using Tomcat then read the instructions here https://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html[https://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html].
|
||||
For example, if you're using Tomcat then read the instructions here https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html[https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html].
|
||||
It's important that you get this working before trying it out with Spring Security
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ A pre-invocation decision on whether the invocation is allowed to proceed is mad
|
||||
[[authz-access-decision-manager]]
|
||||
==== The AccessDecisionManager
|
||||
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]
|
||||
----
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
aspectjVersion=1.9.5
|
||||
gaeVersion=1.9.79
|
||||
springBootVersion=2.2.6.RELEASE
|
||||
version=5.2.3.RELEASE
|
||||
aspectjVersion=1.9.6
|
||||
gaeVersion=1.9.82
|
||||
springBootVersion=2.2.10.RELEASE
|
||||
version=5.2.7.RELEASE
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Dysprosium-SR6'
|
||||
ext.reactorVersion = 'Dysprosium-SR12'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.2.5.RELEASE'
|
||||
ext.springVersion = '5.2.9.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Moore-SR6'
|
||||
ext.springDataVersion = 'Moore-SR10'
|
||||
}
|
||||
|
||||
ext.rsocketVersion = '1.0.0-RC6'
|
||||
ext.rsocketVersion = '1.0.2'
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
@@ -20,17 +20,17 @@ dependencyManagement {
|
||||
}
|
||||
dependencies {
|
||||
dependency 'cglib:cglib-nodep:3.3.0'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.14.7'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.14.9'
|
||||
dependency 'opensymphony:sitemesh:2.4.2'
|
||||
dependency 'org.gebish:geb-spock:0.10.0'
|
||||
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.6'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.6'
|
||||
dependency 'org.powermock:powermock-core:2.0.6'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.6'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.6'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.6'
|
||||
dependency 'org.python:jython:2.5.0'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.7'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.7'
|
||||
dependency 'org.powermock:powermock-core:2.0.7'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.7'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.7'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.7'
|
||||
dependency 'org.python:jython:2.5.3'
|
||||
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
|
||||
}
|
||||
@@ -42,9 +42,9 @@ dependencyManagement {
|
||||
dependency 'asm:asm:3.1'
|
||||
dependency 'ch.qos.logback:logback-classic:1.2.3'
|
||||
dependency 'ch.qos.logback:logback-core:1.2.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.5'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.5'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.5'
|
||||
dependency 'com.fasterxml:classmate:1.3.4'
|
||||
dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1'
|
||||
dependency 'com.google.appengine:appengine-api-1.0-sdk:$gaeVersion'
|
||||
@@ -58,10 +58,10 @@ dependencyManagement {
|
||||
dependency 'com.nimbusds:lang-tag:1.4.3'
|
||||
dependency 'com.nimbusds:nimbus-jose-jwt:7.8.1'
|
||||
dependency 'com.nimbusds:oauth2-oidc-sdk:6.14'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.14.7'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||
dependency 'com.squareup.okio:okio:1.13.0'
|
||||
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
|
||||
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2'
|
||||
dependency 'com.sun.xml.bind:jaxb-impl:2.3.3'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.14'
|
||||
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
|
||||
dependency 'commons-cli:commons-cli:1.4'
|
||||
@@ -72,7 +72,7 @@ dependencyManagement {
|
||||
dependency 'commons-lang:commons-lang:2.6'
|
||||
dependency 'commons-logging:commons-logging:1.2'
|
||||
dependency 'dom4j:dom4j:1.6.1'
|
||||
dependency 'io.projectreactor.tools:blockhound:1.0.3.RELEASE'
|
||||
dependency 'io.projectreactor.tools:blockhound:1.0.4.RELEASE'
|
||||
dependency "io.rsocket:rsocket-core:${rsocketVersion}"
|
||||
dependency "io.rsocket:rsocket-transport-netty:${rsocketVersion}"
|
||||
dependency 'javax.activation:activation:1.1.1'
|
||||
@@ -86,8 +86,8 @@ dependencyManagement {
|
||||
dependency 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
|
||||
dependency 'junit:junit:4.12'
|
||||
dependency 'ldapsdk:ldapsdk:4.1'
|
||||
dependency 'net.bytebuddy:byte-buddy-agent:1.9.10'
|
||||
dependency 'net.bytebuddy:byte-buddy:1.9.10'
|
||||
dependency 'net.bytebuddy:byte-buddy-agent:1.9.16'
|
||||
dependency 'net.bytebuddy:byte-buddy:1.9.16'
|
||||
dependency 'net.jcip:jcip-annotations:1.0'
|
||||
dependency 'net.minidev:accessors-smart:1.2'
|
||||
dependency 'net.minidev:json-smart:2.3'
|
||||
@@ -145,12 +145,12 @@ dependencyManagement {
|
||||
dependency 'org.apache.taglibs:taglibs-standard-impl:1.2.5'
|
||||
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
|
||||
dependency 'org.apache.taglibs:taglibs-standard-spec:1.2.5'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.33'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.33'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.33'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.33'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.33'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.38'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.38'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.38'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.38'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.57'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.38'
|
||||
dependency "org.aspectj:aspectjrt:$aspectjVersion"
|
||||
dependency "org.aspectj:aspectjtools:$aspectjVersion"
|
||||
dependency "org.aspectj:aspectjweaver:$aspectjVersion"
|
||||
@@ -158,9 +158,9 @@ dependencyManagement {
|
||||
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
|
||||
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
|
||||
dependency 'org.bouncycastle:bcprov-jdk15on:1.64'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.20'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.20'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.20'
|
||||
dependency 'org.eclipse.jdt:ecj:3.12.3'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
|
||||
@@ -182,8 +182,8 @@ dependencyManagement {
|
||||
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
|
||||
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.18.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.13.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.2.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.22.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.6.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.5.0'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
|
||||
dependency 'org.javassist:javassist:3.22.0-CR2'
|
||||
|
||||
+17
-4
@@ -16,6 +16,7 @@
|
||||
package org.springframework.security.ldap.authentication.ad;
|
||||
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.ldap.CommunicationException;
|
||||
import org.springframework.ldap.core.DirContextOperations;
|
||||
import org.springframework.ldap.core.DistinguishedName;
|
||||
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.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -142,12 +144,15 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
UsernamePasswordAuthenticationToken auth) {
|
||||
String username = auth.getName();
|
||||
String password = (String) auth.getCredentials();
|
||||
|
||||
DirContext ctx = bindAsUser(username, password);
|
||||
DirContext ctx = null;
|
||||
|
||||
try {
|
||||
ctx = bindAsUser(username, password);
|
||||
return searchForUser(ctx, username);
|
||||
}
|
||||
catch (CommunicationException e) {
|
||||
throw badLdapConnection(e);
|
||||
}
|
||||
catch (NamingException e) {
|
||||
logger.error("Failed to locate directory entry for authenticated user: "
|
||||
+ username, e);
|
||||
@@ -210,8 +215,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
|| (e instanceof OperationNotSupportedException)) {
|
||||
handleBindException(bindPrincipal, e);
|
||||
throw badCredentials(e);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw LdapUtils.convertLdapException(e);
|
||||
}
|
||||
}
|
||||
@@ -313,6 +317,12 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
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)
|
||||
throws NamingException {
|
||||
SearchControls searchControls = new SearchControls();
|
||||
@@ -327,6 +337,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
searchControls, searchRoot, searchFilter,
|
||||
new Object[] { bindPrincipal, username });
|
||||
}
|
||||
catch (CommunicationException ldapCommunicationException) {
|
||||
throw badLdapConnection(ldapCommunicationException);
|
||||
}
|
||||
catch (IncorrectResultSizeDataAccessException incorrectResults) {
|
||||
// Search should never return multiple results if properly configured - just
|
||||
// rethrow
|
||||
|
||||
+27
-7
@@ -32,6 +32,7 @@ import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -58,6 +59,9 @@ import static org.springframework.security.ldap.authentication.ad.ActiveDirector
|
||||
* @author Rob Winch
|
||||
*/
|
||||
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
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@@ -378,16 +382,29 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test(expected = org.springframework.ldap.CommunicationException.class)
|
||||
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() {
|
||||
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
|
||||
msg));
|
||||
provider.authenticate(joe);
|
||||
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Throwable {
|
||||
try {
|
||||
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
|
||||
msg));
|
||||
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
|
||||
public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception {
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -413,8 +430,11 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
provider.authenticate(joe);
|
||||
fail("CommunicationException was expected with a root cause of ClassNotFoundException");
|
||||
}
|
||||
catch (org.springframework.ldap.CommunicationException expected) {
|
||||
assertThat(expected.getRootCause()).isInstanceOf(ClassNotFoundException.class);
|
||||
catch (InternalAuthenticationServiceException expected) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -20,7 +20,11 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -40,6 +44,7 @@ import org.springframework.util.Assert;
|
||||
* @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 OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
|
||||
/**
|
||||
@@ -59,8 +64,18 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
||||
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
|
||||
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
|
||||
|
||||
OAuth2AuthorizationExchangeValidator.validate(
|
||||
authorizationCodeAuthentication.getAuthorizationExchange());
|
||||
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
|
||||
.getAuthorizationExchange().getAuthorizationResponse();
|
||||
if (authorizationResponse.statusError()) {
|
||||
throw new OAuth2AuthorizationException(authorizationResponse.getError());
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
|
||||
.getAuthorizationExchange().getAuthorizationRequest();
|
||||
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthorizationException(oauth2Error);
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse =
|
||||
this.accessTokenResponseClient.getTokenResponse(
|
||||
|
||||
+17
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,9 +22,13 @@ import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessT
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
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.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -55,8 +59,8 @@ import java.util.function.Function;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</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 OAuth2AuthorizationCodeReactiveAuthenticationManager implements
|
||||
ReactiveAuthenticationManager {
|
||||
public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
|
||||
public OAuth2AuthorizationCodeReactiveAuthenticationManager(
|
||||
@@ -70,7 +74,16 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements
|
||||
return Mono.defer(() -> {
|
||||
OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
|
||||
|
||||
OAuth2AuthorizationExchangeValidator.validate(token.getAuthorizationExchange());
|
||||
OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange().getAuthorizationResponse();
|
||||
if (authorizationResponse.statusError()) {
|
||||
return Mono.error(new OAuth2AuthorizationException(authorizationResponse.getError()));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange().getAuthorizationRequest();
|
||||
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
return Mono.error(new OAuth2AuthorizationException(oauth2Error));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
token.getClientRegistration(),
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.client.authentication;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
|
||||
/**
|
||||
* A validator for an "exchange" of an OAuth 2.0 Authorization Request and Response.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.1
|
||||
* @see OAuth2AuthorizationExchange
|
||||
*/
|
||||
final class OAuth2AuthorizationExchangeValidator {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
|
||||
static void validate(OAuth2AuthorizationExchange authorizationExchange) {
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
|
||||
OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
|
||||
|
||||
if (authorizationResponse.statusError()) {
|
||||
throw new OAuth2AuthorizationException(authorizationResponse.getError());
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthorizationException(oauth2Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -121,13 +121,14 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
||||
.getAuthorizationExchange().getAuthorizationResponse();
|
||||
|
||||
if (authorizationResponse.statusError()) {
|
||||
throw new OAuth2AuthenticationException(
|
||||
authorizationResponse.getError(), authorizationResponse.getError().toString());
|
||||
return Mono.error(new OAuth2AuthenticationException(
|
||||
authorizationResponse.getError(), authorizationResponse.getError().toString()));
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
return Mono.error(new OAuth2AuthenticationException(
|
||||
oauth2Error, oauth2Error.toString()));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
@@ -139,7 +140,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
||||
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()))
|
||||
.onErrorMap(JwtException.class, e -> {
|
||||
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, e.getMessage(), null);
|
||||
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e);
|
||||
return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -166,7 +167,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
||||
INVALID_ID_TOKEN_ERROR_CODE,
|
||||
"Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
|
||||
return Mono.error(new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()));
|
||||
}
|
||||
|
||||
return createOidcToken(clientRegistration, accessTokenResponse)
|
||||
|
||||
+12
-4
@@ -27,6 +27,8 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
@@ -173,15 +175,21 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.requiresAuthenticationMatcher.matches(exchange)
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.flatMap(matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.flatMap(matchResult ->
|
||||
this.authenticationConverter.convert(exchange)
|
||||
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(
|
||||
e.getError(), e.getError().toString())))
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap(token -> authenticate(exchange, chain, token));
|
||||
.flatMap(token -> authenticate(exchange, chain, token))
|
||||
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
|
||||
.onAuthenticationFailure(new WebFilterExchange(exchange, chain), e));
|
||||
}
|
||||
|
||||
private Mono<Void> authenticate(ServerWebExchange exchange,
|
||||
WebFilterChain chain, Authentication token) {
|
||||
private Mono<Void> authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
return this.authenticationManager.authenticate(token)
|
||||
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(
|
||||
e.getError(), e.getError().toString()))
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
|
||||
.flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange))
|
||||
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
|
||||
|
||||
+1
-2
@@ -18,7 +18,6 @@ package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -33,7 +32,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Converts from a {@link ServerWebExchange} to an {@link OAuth2LoginAuthenticationToken} that can be authenticated. The
|
||||
* Converts from a {@link ServerWebExchange} to an {@link OAuth2AuthorizationCodeAuthenticationToken} that can be authenticated. The
|
||||
* converter does not validate any errors it only performs a conversion.
|
||||
* @author Rob Winch
|
||||
* @since 5.1
|
||||
|
||||
+59
@@ -29,6 +29,9 @@ import org.springframework.security.oauth2.client.authentication.TestOAuth2Autho
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -41,6 +44,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -234,6 +238,61 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
}
|
||||
|
||||
// gh-8609
|
||||
@Test
|
||||
public void filterWhenAuthenticationConverterThrowsOAuth2AuthorizationExceptionThenMappedToOAuth2AuthenticationException() {
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback");
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
assertThatThrownBy(() -> this.filter.filter(exchange, chain).block())
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("client_registration_not_found");
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
}
|
||||
|
||||
// gh-8609
|
||||
@Test
|
||||
public void filterWhenAuthenticationManagerThrowsOAuth2AuthorizationExceptionThenMappedToOAuth2AuthenticationException() {
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||
.thenReturn(Mono.just(clientRegistration));
|
||||
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback");
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenReturn(Mono.error(new OAuth2AuthorizationException(new OAuth2Error("authorization_error"))));
|
||||
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
assertThatThrownBy(() -> this.filter.filter(exchange, chain).block())
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("authorization_error");
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(
|
||||
MockServerHttpRequest authorizationRequest, ClientRegistration registration) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
+6
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
@@ -28,6 +31,8 @@ import org.springframework.util.Assert;
|
||||
* @since 5.1
|
||||
*/
|
||||
public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static OAuth2Error INVALID_ISSUER =
|
||||
new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
@@ -57,6 +62,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
if (this.issuer.equals(tokenIssuer)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
} else {
|
||||
logger.debug(INVALID_ISSUER.getDescription());
|
||||
return OAuth2TokenValidatorResult.failure(INVALID_ISSUER);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-11
@@ -20,6 +20,9 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
@@ -41,6 +44,8 @@ import org.springframework.util.Assert;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
*/
|
||||
public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static final Duration DEFAULT_MAX_CLOCK_SKEW = Duration.of(60, ChronoUnit.SECONDS);
|
||||
|
||||
private final Duration clockSkew;
|
||||
@@ -56,7 +61,6 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
public JwtTimestampValidator(Duration clockSkew) {
|
||||
Assert.notNull(clockSkew, "clockSkew cannot be null");
|
||||
|
||||
this.clockSkew = clockSkew;
|
||||
}
|
||||
|
||||
@@ -71,11 +75,8 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
if (expiry != null) {
|
||||
if (Instant.now(this.clock).minus(clockSkew).isAfter(expiry)) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
String.format("Jwt expired at %s", jwt.getExpiresAt()),
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt()));
|
||||
return OAuth2TokenValidatorResult.failure(oAuth2Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,17 +84,22 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
if (notBefore != null) {
|
||||
if (Instant.now(this.clock).plus(clockSkew).isBefore(notBefore)) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
String.format("Jwt used before %s", jwt.getNotBefore()),
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore()));
|
||||
return OAuth2TokenValidatorResult.failure(oAuth2Error);
|
||||
}
|
||||
}
|
||||
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
|
||||
private OAuth2Error createOAuth2Error(String reason) {
|
||||
logger.debug(reason);
|
||||
return new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
reason,
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
}
|
||||
|
||||
/**
|
||||
* '
|
||||
* Use this {@link Clock} with {@link Instant#now()} for assessing
|
||||
|
||||
+13
-2
@@ -22,6 +22,7 @@ import java.net.URL;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -54,11 +55,13 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@@ -164,9 +167,17 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
private Jwt validateJwt(Jwt jwt){
|
||||
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
|
||||
if (result.hasErrors()) {
|
||||
String description = result.getErrors().iterator().next().getDescription();
|
||||
Collection<OAuth2Error> errors = result.getErrors();
|
||||
String validationErrorString = "Unable to validate Jwt";
|
||||
for (OAuth2Error oAuth2Error : errors) {
|
||||
if (!StringUtils.isEmpty(oAuth2Error.getDescription())) {
|
||||
validationErrorString = String.format(
|
||||
DECODING_ERROR_MESSAGE_TEMPLATE, oAuth2Error.getDescription());
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new JwtValidationException(
|
||||
String.format(DECODING_ERROR_MESSAGE_TEMPLATE, description),
|
||||
validationErrorString,
|
||||
result.getErrors());
|
||||
}
|
||||
|
||||
|
||||
+13
-4
@@ -16,6 +16,7 @@
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -51,12 +52,14 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
@@ -172,10 +175,16 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
|
||||
private Jwt validateJwt(Jwt jwt) {
|
||||
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
|
||||
|
||||
if ( result.hasErrors() ) {
|
||||
String message = result.getErrors().iterator().next().getDescription();
|
||||
throw new JwtValidationException(message, result.getErrors());
|
||||
if (result.hasErrors()) {
|
||||
Collection<OAuth2Error> errors = result.getErrors();
|
||||
String validationErrorString = "Unable to validate Jwt";
|
||||
for (OAuth2Error oAuth2Error : errors) {
|
||||
if (!StringUtils.isEmpty(oAuth2Error.getDescription())) {
|
||||
validationErrorString = oAuth2Error.getDescription();
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new JwtValidationException(validationErrorString, errors);
|
||||
}
|
||||
|
||||
return jwt;
|
||||
|
||||
+16
@@ -193,6 +193,22 @@ public class NimbusJwtDecoderTests {
|
||||
.hasFieldOrPropertyWithValue("errors", Arrays.asList(firstFailure, secondFailure));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenReadingErrorPickTheFirstErrorMessage() {
|
||||
OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
|
||||
this.jwtDecoder.setJwtValidator(jwtValidator);
|
||||
|
||||
OAuth2Error errorEmpty = new OAuth2Error("mock-error", "", "mock-uri");
|
||||
OAuth2Error error = new OAuth2Error("mock-error", "mock-description", "mock-uri");
|
||||
OAuth2Error error2 = new OAuth2Error("mock-error-second", "mock-description-second", "mock-uri-second");
|
||||
OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(errorEmpty, error, error2);
|
||||
when(jwtValidator.validate(any(Jwt.class))).thenReturn(result);
|
||||
|
||||
Assertions.assertThatCode(() -> this.jwtDecoder.decode(SIGNED_JWT))
|
||||
.isInstanceOf(JwtValidationException.class)
|
||||
.hasMessageContaining("mock-description");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenUsingSignedJwtThenReturnsClaimsGivenByClaimSetConverter() {
|
||||
Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = mock(Converter.class);
|
||||
|
||||
+16
@@ -221,6 +221,22 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
.hasMessageContaining("mock-description");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenReadingErrorPickTheFirstErrorMessage() {
|
||||
OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
|
||||
this.decoder.setJwtValidator(jwtValidator);
|
||||
|
||||
OAuth2Error errorEmpty = new OAuth2Error("mock-error", "", "mock-uri");
|
||||
OAuth2Error error = new OAuth2Error("mock-error", "mock-description", "mock-uri");
|
||||
OAuth2Error error2 = new OAuth2Error("mock-error-second", "mock-description-second", "mock-uri-second");
|
||||
OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(errorEmpty, error, error2);
|
||||
when(jwtValidator.validate(any(Jwt.class))).thenReturn(result);
|
||||
|
||||
assertThatCode(() -> this.decoder.decode(this.messageReadToken).block())
|
||||
.isInstanceOf(JwtValidationException.class)
|
||||
.hasMessageContaining("mock-description");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenUsingSignedJwtThenReturnsClaimsGivenByClaimSetConverter() {
|
||||
Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = mock(Converter.class);
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ public final class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Coll
|
||||
* @since 5.2
|
||||
*/
|
||||
public void setAuthorityPrefix(String authorityPrefix) {
|
||||
Assert.hasText(authorityPrefix, "authorityPrefix cannot be empty");
|
||||
Assert.notNull(authorityPrefix, "authorityPrefix cannot be null");
|
||||
this.authorityPrefix = authorityPrefix;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ import org.springframework.util.StringUtils;
|
||||
public final class DefaultBearerTokenResolver implements BearerTokenResolver {
|
||||
|
||||
private static final Pattern authorizationPattern = Pattern.compile(
|
||||
"^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$",
|
||||
"^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private boolean allowFormEncodedBodyParameter = false;
|
||||
|
||||
+2
-2
@@ -44,13 +44,13 @@ import java.util.regex.Pattern;
|
||||
public class ServerBearerTokenAuthenticationConverter
|
||||
implements ServerAuthenticationConverter {
|
||||
private static final Pattern authorizationPattern = Pattern.compile(
|
||||
"^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$",
|
||||
"^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private boolean allowUriQueryParameter = false;
|
||||
|
||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||
return Mono.justOrEmpty(token(exchange.getRequest()))
|
||||
return Mono.fromCallable(() -> token(exchange.getRequest()))
|
||||
.map(token -> {
|
||||
if (token.isEmpty()) {
|
||||
BearerTokenError error = invalidTokenError();
|
||||
|
||||
+32
@@ -37,6 +37,12 @@ import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
|
||||
*/
|
||||
public class JwtGrantedAuthoritiesConverterTests {
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void setAuthorityPrefixWithNullThenException() {
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
|
||||
Jwt jwt = jwt().claim("scope", "message:read message:write").build();
|
||||
@@ -62,6 +68,19 @@ public class JwtGrantedAuthoritiesConverterTests {
|
||||
new SimpleGrantedAuthority("ROLE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWithBlankAsCustomAuthorityPrefixWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
|
||||
Jwt jwt = jwt().claim("scope", "message:read message:write").build();
|
||||
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
|
||||
assertThat(authorities).containsExactly(
|
||||
new SimpleGrantedAuthority("message:read"),
|
||||
new SimpleGrantedAuthority("message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() {
|
||||
Jwt jwt = jwt().claim("scope", "").build();
|
||||
@@ -97,6 +116,19 @@ public class JwtGrantedAuthoritiesConverterTests {
|
||||
new SimpleGrantedAuthority("ROLE_message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWithBlankAsCustomAuthorityPrefixWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
|
||||
Jwt jwt = jwt().claim("scp", "message:read message:write").build();
|
||||
|
||||
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
|
||||
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
|
||||
|
||||
assertThat(authorities).containsExactly(
|
||||
new SimpleGrantedAuthority("message:read"),
|
||||
new SimpleGrantedAuthority("message:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() {
|
||||
Jwt jwt = jwt().claim("scp", Collections.emptyList()).build();
|
||||
|
||||
+11
-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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,6 +51,16 @@ public class DefaultBearerTokenResolverTests {
|
||||
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
|
||||
}
|
||||
|
||||
// gh-8502
|
||||
@Test
|
||||
public void resolveWhenHeaderEndsWithPaddingIndicatorThenTokenIsResolved() {
|
||||
String token = TEST_TOKEN + "==";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isEqualTo(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenLowercaseHeaderIsPresentThenTokenIsResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
+26
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,8 +16,11 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.resource.web.server;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
@@ -27,8 +30,6 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
||||
@@ -56,6 +57,17 @@ public class ServerBearerTokenAuthenticationConverterTests {
|
||||
assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN);
|
||||
}
|
||||
|
||||
// gh-8502
|
||||
@Test
|
||||
public void resolveWhenHeaderEndsWithPaddingIndicatorThenTokenIsResolved() {
|
||||
String token = TEST_TOKEN + "==";
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest
|
||||
.get("/")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
|
||||
assertThat(convertToToken(request).getToken()).isEqualTo(token);
|
||||
}
|
||||
|
||||
// gh-7011
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsEmptyStringThenTokenIsResolved() {
|
||||
@@ -119,6 +131,17 @@ public class ServerBearerTokenAuthenticationConverterTests {
|
||||
.hasMessageContaining(("Bearer token is malformed"));
|
||||
}
|
||||
|
||||
// gh-8865
|
||||
@Test
|
||||
public void resolveWhenHeaderWithInvalidCharactersIsPresentAndNotSubscribedThenNoneExceptionIsThrown() {
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest
|
||||
.get("/")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer an\"invalid\"token");
|
||||
|
||||
assertThatCode(() -> this.converter.convert(MockServerWebExchange.from(request)))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ class PayloadSocketAcceptor implements SocketAcceptor {
|
||||
return intercept(setup, dataMimeType, metadataMimeType)
|
||||
.flatMap(ctx -> this.delegate.accept(setup, sendingSocket)
|
||||
.map(acceptingSocket -> new PayloadInterceptorRSocket(acceptingSocket, this.interceptors, metadataMimeType, dataMimeType, ctx))
|
||||
.subscriberContext(ctx)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 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.rsocket.core;
|
||||
|
||||
import io.rsocket.ConnectionSetupPayload;
|
||||
import io.rsocket.RSocket;
|
||||
import io.rsocket.SocketAcceptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
|
||||
/**
|
||||
* A {@link SocketAcceptor} that captures the {@link SecurityContext} and then continues with the {@link RSocket}
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class CaptureSecurityContextSocketAcceptor implements SocketAcceptor {
|
||||
private final RSocket accept;
|
||||
|
||||
private SecurityContext securityContext;
|
||||
|
||||
CaptureSecurityContextSocketAcceptor(RSocket accept) {
|
||||
this.accept = accept;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RSocket> accept(ConnectionSetupPayload setup, RSocket sendingSocket) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.doOnNext(securityContext -> this.securityContext = securityContext)
|
||||
.thenReturn(this.accept);
|
||||
}
|
||||
|
||||
public SecurityContext getSecurityContext() {
|
||||
return this.securityContext;
|
||||
}
|
||||
}
|
||||
+32
-7
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.security.rsocket.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.rsocket.ConnectionSetupPayload;
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.RSocket;
|
||||
@@ -27,16 +31,16 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.rsocket.api.PayloadExchange;
|
||||
import org.springframework.security.rsocket.api.PayloadInterceptor;
|
||||
import org.springframework.security.rsocket.core.PayloadInterceptorRSocket;
|
||||
import org.springframework.security.rsocket.core.PayloadSocketAcceptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
@@ -144,6 +148,27 @@ public class PayloadSocketAcceptorTests {
|
||||
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
// gh-8654
|
||||
public void acceptWhenDelegateAcceptRequiresReactiveSecurityContext() {
|
||||
when(this.setupPayload.metadataMimeType()).thenReturn(MediaType.TEXT_PLAIN_VALUE);
|
||||
when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE);
|
||||
SecurityContext expectedSecurityContext = new SecurityContextImpl(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
|
||||
CaptureSecurityContextSocketAcceptor captureSecurityContext = new CaptureSecurityContextSocketAcceptor(this.rSocket);
|
||||
PayloadInterceptor authenticateInterceptor = (exchange, chain) -> {
|
||||
Context withSecurityContext = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(expectedSecurityContext));
|
||||
return chain.next(exchange)
|
||||
.subscriberContext(withSecurityContext);
|
||||
};
|
||||
List<PayloadInterceptor> interceptors = Arrays.asList(authenticateInterceptor);
|
||||
this.acceptor = new PayloadSocketAcceptor(captureSecurityContext, interceptors);
|
||||
|
||||
this.acceptor.accept(this.setupPayload, this.rSocket).block();
|
||||
|
||||
assertThat(captureSecurityContext.getSecurityContext()).isEqualTo(expectedSecurityContext);
|
||||
}
|
||||
|
||||
private PayloadExchange captureExchange() {
|
||||
when(this.delegate.accept(any(), any())).thenReturn(Mono.just(this.rSocket));
|
||||
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
+262
-241
@@ -25,13 +25,31 @@ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMap
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.opensaml.saml.common.SignableSAMLObject;
|
||||
import org.opensaml.saml.common.assertion.AssertionValidationException;
|
||||
import org.opensaml.core.criterion.EntityIdCriterion;
|
||||
|
||||
import org.opensaml.saml.common.assertion.ValidationContext;
|
||||
import org.opensaml.saml.common.assertion.ValidationResult;
|
||||
import org.opensaml.saml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml.criterion.ProtocolCriterion;
|
||||
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
|
||||
import org.opensaml.saml.saml2.assertion.ConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
|
||||
@@ -51,40 +69,36 @@ import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
|
||||
import org.opensaml.security.credential.Credential;
|
||||
import org.opensaml.security.credential.CredentialResolver;
|
||||
import org.opensaml.security.credential.CredentialSupport;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
|
||||
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
|
||||
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
|
||||
import org.opensaml.security.criteria.UsageCriterion;
|
||||
import org.opensaml.security.x509.BasicX509Credential;
|
||||
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
|
||||
import org.opensaml.xmlsec.encryption.support.DecryptionException;
|
||||
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
|
||||
import org.opensaml.xmlsec.keyinfo.impl.StaticKeyInfoCredentialResolver;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureException;
|
||||
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureValidator;
|
||||
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Collections.singleton;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.CLOCK_SKEW;
|
||||
import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.COND_VALID_AUDIENCES;
|
||||
import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.SIGNATURE_REQUIRED;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.DECRYPTION_ERROR;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_ASSERTION;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_DESTINATION;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_ISSUER;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_SIGNATURE;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.MALFORMED_RESPONSE_DATA;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.SUBJECT_NOT_FOUND;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS;
|
||||
import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.USERNAME_NOT_FOUND;
|
||||
import static org.springframework.util.Assert.notNull;
|
||||
import static org.springframework.util.StringUtils.hasText;
|
||||
|
||||
/**
|
||||
* Implementation of {@link AuthenticationProvider} for SAML authentications when receiving a
|
||||
@@ -126,6 +140,20 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
|
||||
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class);
|
||||
|
||||
private final List<ConditionValidator> conditions = Collections.singletonList(new AudienceRestrictionConditionValidator());
|
||||
private final SubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator() {
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation,
|
||||
@Nonnull Assertion assertion, @Nonnull ValidationContext context) {
|
||||
// skipping address validation - gh-7514
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
};
|
||||
private final List<SubjectConfirmationValidator> subjects = Collections.singletonList(this.subjectConfirmationValidator);
|
||||
private final List<StatementValidator> statements = Collections.emptyList();
|
||||
private final SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator();
|
||||
|
||||
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance();
|
||||
private Converter<Assertion, Collection<? extends GrantedAuthority>> authoritiesExtractor =
|
||||
(a -> singletonList(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
@@ -174,17 +202,17 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
try {
|
||||
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
|
||||
Response samlResponse = getSaml2Response(token);
|
||||
Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse);
|
||||
Response response = parse(token.getSaml2Response());
|
||||
List<Assertion> validAssertions = validateResponse(token, response);
|
||||
Assertion assertion = validAssertions.get(0);
|
||||
String username = getUsername(token, assertion);
|
||||
return new Saml2Authentication(
|
||||
new SimpleSaml2AuthenticatedPrincipal(username), token.getSaml2Response(),
|
||||
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))
|
||||
);
|
||||
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)));
|
||||
} catch (Saml2AuthenticationException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e);
|
||||
throw authException(INTERNAL_VALIDATION_ERROR, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,167 +228,9 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
return this.authoritiesExtractor.convert(assertion);
|
||||
}
|
||||
|
||||
private String getUsername(Saml2AuthenticationToken token, Assertion assertion) throws Saml2AuthenticationException {
|
||||
String username = null;
|
||||
Subject subject = assertion.getSubject();
|
||||
if (subject == null) {
|
||||
throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject");
|
||||
}
|
||||
if (subject.getNameID() != null) {
|
||||
username = subject.getNameID().getValue();
|
||||
}
|
||||
else if (subject.getEncryptedID() != null) {
|
||||
NameID nameId = decrypt(token, subject.getEncryptedID());
|
||||
username = nameId.getValue();
|
||||
}
|
||||
if (username == null) {
|
||||
throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier");
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
private Assertion validateSaml2Response(Saml2AuthenticationToken token,
|
||||
String recipient,
|
||||
Response samlResponse) throws Saml2AuthenticationException {
|
||||
//optional validation if the response contains a destination
|
||||
if (hasText(samlResponse.getDestination()) && !recipient.equals(samlResponse.getDestination())) {
|
||||
throw authException(INVALID_DESTINATION, "Invalid SAML response destination: " + samlResponse.getDestination());
|
||||
}
|
||||
|
||||
String issuer = samlResponse.getIssuer().getValue();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Validating SAML response from " + issuer);
|
||||
}
|
||||
if (!hasText(issuer) || (!issuer.equals(token.getIdpEntityId()))) {
|
||||
String message = String.format("Response issuer '%s' doesn't match '%s'", issuer, token.getIdpEntityId());
|
||||
throw authException(INVALID_ISSUER, message);
|
||||
}
|
||||
Saml2AuthenticationException lastValidationError = null;
|
||||
|
||||
boolean responseSigned = hasValidSignature(samlResponse, token);
|
||||
for (Assertion a : samlResponse.getAssertions()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Checking plain assertion validity " + a);
|
||||
}
|
||||
try {
|
||||
validateAssertion(recipient, a, token, !responseSigned);
|
||||
return a;
|
||||
} catch (Saml2AuthenticationException e) {
|
||||
lastValidationError = e;
|
||||
}
|
||||
}
|
||||
for (EncryptedAssertion ea : samlResponse.getEncryptedAssertions()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Checking encrypted assertion validity " + ea);
|
||||
}
|
||||
try {
|
||||
Assertion a = decrypt(token, ea);
|
||||
validateAssertion(recipient, a, token, !responseSigned);
|
||||
return a;
|
||||
} catch (Saml2AuthenticationException e) {
|
||||
lastValidationError = e;
|
||||
}
|
||||
}
|
||||
if (lastValidationError != null) {
|
||||
throw lastValidationError;
|
||||
}
|
||||
else {
|
||||
throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasValidSignature(SignableSAMLObject samlObject, Saml2AuthenticationToken token) {
|
||||
if (!samlObject.isSigned()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("SAML object is not signed, no signatures found");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<X509Certificate> verificationKeys = getVerificationCertificates(token);
|
||||
if (verificationKeys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (X509Certificate certificate : verificationKeys) {
|
||||
Credential credential = getVerificationCredential(certificate);
|
||||
try {
|
||||
SignatureValidator.validate(samlObject.getSignature(), credential);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Valid signature found in SAML object:"+samlObject.getClass().getName());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (SignatureException ignored) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Signature validation failed with cert:"+certificate.toString(), ignored);
|
||||
}
|
||||
else if (logger.isDebugEnabled()) {
|
||||
logger.debug("Signature validation failed with cert:"+certificate.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void validateAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) {
|
||||
SAML20AssertionValidator validator = getAssertionValidator(token);
|
||||
Map<String, Object> validationParams = new HashMap<>();
|
||||
validationParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false);
|
||||
validationParams.put(
|
||||
SAML2AssertionValidationParameters.CLOCK_SKEW,
|
||||
this.responseTimeValidationSkew.toMillis()
|
||||
);
|
||||
validationParams.put(
|
||||
SAML2AssertionValidationParameters.COND_VALID_AUDIENCES,
|
||||
singleton(token.getLocalSpEntityId())
|
||||
);
|
||||
if (hasText(recipient)) {
|
||||
validationParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, singleton(recipient));
|
||||
}
|
||||
|
||||
if (signatureRequired && !hasValidSignature(a, token)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(format("Assertion [%s] does not a valid signature.", a.getID()));
|
||||
}
|
||||
throw authException(Saml2ErrorCodes.INVALID_SIGNATURE, "Assertion doesn't have a valid signature.");
|
||||
}
|
||||
//ensure that OpenSAML doesn't attempt signature validation, already performed
|
||||
a.setSignature(null);
|
||||
|
||||
//ensure that we don't validate IP addresses as part of our validation gh-7514
|
||||
if (a.getSubject() != null) {
|
||||
for (SubjectConfirmation sc : a.getSubject().getSubjectConfirmations()) {
|
||||
if (sc.getSubjectConfirmationData() != null) {
|
||||
sc.getSubjectConfirmationData().setAddress(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//remainder of assertion validation
|
||||
ValidationContext vctx = new ValidationContext(validationParams);
|
||||
private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException {
|
||||
try {
|
||||
ValidationResult result = validator.validate(a, vctx);
|
||||
boolean valid = result.equals(ValidationResult.VALID);
|
||||
if (!valid) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(format("Failed to validate assertion from %s", token.getIdpEntityId()));
|
||||
}
|
||||
throw authException(Saml2ErrorCodes.INVALID_ASSERTION, vctx.getValidationFailureMessage());
|
||||
}
|
||||
}
|
||||
catch (AssertionValidationException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to validate assertion:", e);
|
||||
}
|
||||
throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Response getSaml2Response(Saml2AuthenticationToken token) throws Saml2Exception, Saml2AuthenticationException {
|
||||
try {
|
||||
Object result = this.saml.resolve(token.getSaml2Response());
|
||||
Object result = this.saml.resolve(response);
|
||||
if (result instanceof Response) {
|
||||
return (Response) result;
|
||||
}
|
||||
@@ -373,68 +243,172 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
|
||||
}
|
||||
|
||||
private Saml2Error validationError(String code, String description) {
|
||||
return new Saml2Error(
|
||||
code,
|
||||
description
|
||||
);
|
||||
private List<Assertion> validateResponse(Saml2AuthenticationToken token, Response response)
|
||||
throws Saml2AuthenticationException {
|
||||
|
||||
List<Assertion> validAssertions = new ArrayList<>();
|
||||
String issuer = response.getIssuer().getValue();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Validating SAML response from " + issuer);
|
||||
}
|
||||
|
||||
List<Assertion> assertions = new ArrayList<>(response.getAssertions());
|
||||
for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) {
|
||||
Assertion assertion = decrypt(token, encryptedAssertion);
|
||||
assertions.add(assertion);
|
||||
}
|
||||
if (assertions.isEmpty()) {
|
||||
throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response.");
|
||||
}
|
||||
|
||||
if (!isSigned(response, assertions)) {
|
||||
throw authException(INVALID_SIGNATURE, "Either the response or one of the assertions is unsigned. " +
|
||||
"Please either sign the response or all of the assertions.");
|
||||
}
|
||||
|
||||
SignatureTrustEngine signatureTrustEngine = buildSignatureTrustEngine(token);
|
||||
|
||||
Map<String, Saml2AuthenticationException> validationExceptions = new HashMap<>();
|
||||
if (response.isSigned()) {
|
||||
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
|
||||
try {
|
||||
profileValidator.validate(response.getSignature());
|
||||
} catch (Exception e) {
|
||||
validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE,
|
||||
"Invalid signature for SAML Response [" + response.getID() + "]", e));
|
||||
}
|
||||
|
||||
try {
|
||||
CriteriaSet criteriaSet = new CriteriaSet();
|
||||
criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer)));
|
||||
criteriaSet.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)));
|
||||
criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
|
||||
if (!signatureTrustEngine.validate(response.getSignature(), criteriaSet)) {
|
||||
validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE,
|
||||
"Invalid signature for SAML Response [" + response.getID() + "]"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE,
|
||||
"Invalid signature for SAML Response [" + response.getID() + "]", e));
|
||||
}
|
||||
}
|
||||
|
||||
String destination = response.getDestination();
|
||||
if (StringUtils.hasText(destination) && !destination.equals(token.getRecipientUri())) {
|
||||
String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID() + "]";
|
||||
validationExceptions.put(INVALID_DESTINATION, authException(INVALID_DESTINATION, message));
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(issuer) || !issuer.equals(token.getIdpEntityId())) {
|
||||
String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID());
|
||||
validationExceptions.put(INVALID_ISSUER, authException(INVALID_ISSUER, message));
|
||||
}
|
||||
|
||||
SAML20AssertionValidator validator = buildSamlAssertionValidator(signatureTrustEngine);
|
||||
ValidationContext context = buildValidationContext(token, response);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Validating " + assertions.size() + " assertions");
|
||||
}
|
||||
for (Assertion assertion : assertions) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Validating assertion " + assertion.getID());
|
||||
}
|
||||
try {
|
||||
validAssertions.add(validateAssertion(assertion, validator, context));
|
||||
} catch (Exception e) {
|
||||
String message = String.format("Invalid assertion [%s] for SAML response [%s]", assertion.getID(), response.getID());
|
||||
validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message, e));
|
||||
}
|
||||
}
|
||||
|
||||
if (validationExceptions.isEmpty()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Successfully validated SAML Response [" + response.getID() + "]");
|
||||
}
|
||||
} else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.debug("Found " + validationExceptions.size() + " validation errors in SAML response [" + response.getID() + "]: " +
|
||||
validationExceptions.values());
|
||||
} else if (logger.isDebugEnabled()) {
|
||||
logger.debug("Found " + validationExceptions.size() + " validation errors in SAML response [" + response.getID() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (!validationExceptions.isEmpty()) {
|
||||
throw validationExceptions.values().iterator().next();
|
||||
}
|
||||
if (validAssertions.isEmpty()) {
|
||||
throw authException(MALFORMED_RESPONSE_DATA, "No valid assertions found in response.");
|
||||
}
|
||||
|
||||
return validAssertions;
|
||||
}
|
||||
|
||||
private Saml2AuthenticationException authException(String code, String description) throws Saml2AuthenticationException {
|
||||
return new Saml2AuthenticationException(
|
||||
validationError(code, description)
|
||||
);
|
||||
private boolean isSigned(Response samlResponse, List<Assertion> assertions) {
|
||||
if (samlResponse.isSigned()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Assertion assertion : assertions) {
|
||||
if (!assertion.isSigned()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private Saml2AuthenticationException authException(String code, String description, Exception cause) throws Saml2AuthenticationException {
|
||||
return new Saml2AuthenticationException(
|
||||
validationError(code, description),
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
private SAML20AssertionValidator getAssertionValidator(Saml2AuthenticationToken provider) {
|
||||
List<ConditionValidator> conditions = Collections.singletonList(new AudienceRestrictionConditionValidator());
|
||||
BearerSubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator();
|
||||
|
||||
List<SubjectConfirmationValidator> subjects = Collections.singletonList(subjectConfirmationValidator);
|
||||
List<StatementValidator> statements = Collections.emptyList();
|
||||
|
||||
private SignatureTrustEngine buildSignatureTrustEngine(Saml2AuthenticationToken token) {
|
||||
Set<Credential> credentials = new HashSet<>();
|
||||
for (X509Certificate key : getVerificationCertificates(provider)) {
|
||||
Credential cred = getVerificationCredential(key);
|
||||
for (X509Certificate key : getVerificationCertificates(token)) {
|
||||
BasicX509Credential cred = new BasicX509Credential(key);
|
||||
cred.setUsageType(UsageType.SIGNING);
|
||||
cred.setEntityId(token.getIdpEntityId());
|
||||
credentials.add(cred);
|
||||
}
|
||||
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
|
||||
SignatureTrustEngine signatureTrustEngine = new ExplicitKeySignatureTrustEngine(
|
||||
return new ExplicitKeySignatureTrustEngine(
|
||||
credentialsResolver,
|
||||
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()
|
||||
);
|
||||
SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator();
|
||||
}
|
||||
|
||||
private ValidationContext buildValidationContext(Saml2AuthenticationToken token, Response response) {
|
||||
Map<String, Object> validationParams = new HashMap<>();
|
||||
validationParams.put(SIGNATURE_REQUIRED, !response.isSigned());
|
||||
validationParams.put(CLOCK_SKEW, this.responseTimeValidationSkew.toMillis());
|
||||
validationParams.put(COND_VALID_AUDIENCES, singleton(token.getLocalSpEntityId()));
|
||||
if (StringUtils.hasText(token.getRecipientUri())) {
|
||||
validationParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, singleton(token.getRecipientUri()));
|
||||
}
|
||||
return new ValidationContext(validationParams);
|
||||
}
|
||||
|
||||
private SAML20AssertionValidator buildSamlAssertionValidator(SignatureTrustEngine signatureTrustEngine) {
|
||||
return new SAML20AssertionValidator(
|
||||
conditions,
|
||||
subjects,
|
||||
statements,
|
||||
signatureTrustEngine,
|
||||
signaturePrevalidator
|
||||
);
|
||||
this.conditions, this.subjects, this.statements, signatureTrustEngine, this.signaturePrevalidator);
|
||||
}
|
||||
|
||||
private Credential getVerificationCredential(X509Certificate certificate) {
|
||||
return CredentialSupport.getSimpleCredential(certificate, null);
|
||||
}
|
||||
private Assertion validateAssertion(Assertion assertion,
|
||||
SAML20AssertionValidator validator, ValidationContext context) {
|
||||
|
||||
private Decrypter getDecrypter(Saml2X509Credential key) {
|
||||
Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
|
||||
KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential);
|
||||
Decrypter decrypter = new Decrypter(null, resolver, this.saml.getEncryptedKeyResolver());
|
||||
decrypter.setRootInNewDocument(true);
|
||||
return decrypter;
|
||||
ValidationResult result;
|
||||
try {
|
||||
result = validator.validate(assertion, context);
|
||||
} catch (Exception e) {
|
||||
throw new Saml2Exception("An error occurred while validation the assertion", e);
|
||||
}
|
||||
if (result != ValidationResult.VALID) {
|
||||
throw new Saml2Exception("An error occurred while validating the assertion: " +
|
||||
context.getValidationFailureMessage());
|
||||
}
|
||||
return assertion;
|
||||
}
|
||||
|
||||
private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion)
|
||||
throws Saml2AuthenticationException {
|
||||
|
||||
Saml2AuthenticationException last = null;
|
||||
List<Saml2X509Credential> decryptionCredentials = getDecryptionCredentials(token);
|
||||
if (decryptionCredentials.isEmpty()) {
|
||||
@@ -452,22 +426,12 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
throw last;
|
||||
}
|
||||
|
||||
private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) throws Saml2AuthenticationException {
|
||||
Saml2AuthenticationException last = null;
|
||||
List<Saml2X509Credential> decryptionCredentials = getDecryptionCredentials(token);
|
||||
if (decryptionCredentials.isEmpty()) {
|
||||
throw authException(DECRYPTION_ERROR, "No valid decryption credentials found.");
|
||||
}
|
||||
for (Saml2X509Credential key : decryptionCredentials) {
|
||||
Decrypter decrypter = getDecrypter(key);
|
||||
try {
|
||||
return (NameID) decrypter.decrypt(assertion);
|
||||
}
|
||||
catch (DecryptionException e) {
|
||||
last = authException(DECRYPTION_ERROR, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
throw last;
|
||||
private Decrypter getDecrypter(Saml2X509Credential key) {
|
||||
Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey());
|
||||
KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential);
|
||||
Decrypter decrypter = new Decrypter(null, resolver, this.saml.getEncryptedKeyResolver());
|
||||
decrypter.setRootInNewDocument(true);
|
||||
return decrypter;
|
||||
}
|
||||
|
||||
private List<Saml2X509Credential> getDecryptionCredentials(Saml2AuthenticationToken token) {
|
||||
@@ -489,4 +453,61 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getUsername(Saml2AuthenticationToken token, Assertion assertion)
|
||||
throws Saml2AuthenticationException {
|
||||
|
||||
String username = null;
|
||||
Subject subject = assertion.getSubject();
|
||||
if (subject == null) {
|
||||
throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject");
|
||||
}
|
||||
if (subject.getNameID() != null) {
|
||||
username = subject.getNameID().getValue();
|
||||
}
|
||||
else if (subject.getEncryptedID() != null) {
|
||||
NameID nameId = decrypt(token, subject.getEncryptedID());
|
||||
username = nameId.getValue();
|
||||
}
|
||||
if (username == null) {
|
||||
throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier");
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion)
|
||||
throws Saml2AuthenticationException {
|
||||
|
||||
Saml2AuthenticationException last = null;
|
||||
List<Saml2X509Credential> decryptionCredentials = getDecryptionCredentials(token);
|
||||
if (decryptionCredentials.isEmpty()) {
|
||||
throw authException(DECRYPTION_ERROR, "No valid decryption credentials found.");
|
||||
}
|
||||
for (Saml2X509Credential key : decryptionCredentials) {
|
||||
Decrypter decrypter = getDecrypter(key);
|
||||
try {
|
||||
return (NameID) decrypter.decrypt(assertion);
|
||||
}
|
||||
catch (DecryptionException e) {
|
||||
last = authException(DECRYPTION_ERROR, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
throw last;
|
||||
}
|
||||
|
||||
private Saml2Error validationError(String code, String description) {
|
||||
return new Saml2Error(code, description);
|
||||
}
|
||||
|
||||
private Saml2AuthenticationException authException(String code, String description)
|
||||
throws Saml2AuthenticationException {
|
||||
|
||||
return new Saml2AuthenticationException(validationError(code, description));
|
||||
}
|
||||
|
||||
private Saml2AuthenticationException authException(String code, String description, Exception cause)
|
||||
throws Saml2AuthenticationException {
|
||||
|
||||
return new Saml2AuthenticationException(validationError(code, description), cause);
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -116,6 +116,13 @@ public class OpenSamlAuthenticationProviderTests {
|
||||
@Test
|
||||
public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
|
||||
Response response = response(recipientUri + "invalid", idpEntityId);
|
||||
Assertion assertion = defaultAssertion();
|
||||
signXmlObject(
|
||||
assertion,
|
||||
assertingPartyCredentials(),
|
||||
recipientEntityId
|
||||
);
|
||||
response.getAssertions().add(assertion);
|
||||
token = responseXml(response, idpEntityId);
|
||||
exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION));
|
||||
provider.authenticate(token);
|
||||
|
||||
+5
-7
@@ -77,7 +77,6 @@ import javax.servlet.http.HttpSession;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildConditions;
|
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildIssuer;
|
||||
@@ -237,8 +236,8 @@ public class Saml2LoginIntegrationTests {
|
||||
sendResponse(response, "/login?error")
|
||||
.andExpect(
|
||||
saml2AuthenticationExceptionMatcher(
|
||||
"invalid_signature",
|
||||
equalTo("Assertion doesn't have a valid signature.")
|
||||
"invalid_assertion",
|
||||
containsString("Invalid assertion [assertion] for SAML response")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -253,7 +252,7 @@ public class Saml2LoginIntegrationTests {
|
||||
.andExpect(
|
||||
saml2AuthenticationExceptionMatcher(
|
||||
"invalid_assertion",
|
||||
containsString("Assertion 'assertion' with NotOnOrAfter condition of")
|
||||
containsString("Invalid assertion [assertion] for SAML response")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -268,7 +267,7 @@ public class Saml2LoginIntegrationTests {
|
||||
.andExpect(
|
||||
saml2AuthenticationExceptionMatcher(
|
||||
"invalid_assertion",
|
||||
containsString("Assertion 'assertion' with NotBefore condition of")
|
||||
containsString("Invalid assertion [assertion] for SAML response")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -285,8 +284,7 @@ public class Saml2LoginIntegrationTests {
|
||||
saml2AuthenticationExceptionMatcher(
|
||||
"invalid_issuer",
|
||||
containsString(
|
||||
"Response issuer 'invalid issuer' doesn't match "+
|
||||
"'https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php'"
|
||||
"Invalid issuer [invalid issuer] for SAML response"
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
+66
-11
@@ -15,16 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.test.web.servlet.request;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import org.springframework.beans.Mergeable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
||||
@@ -86,15 +88,23 @@ public final class SecurityMockMvcRequestBuilders {
|
||||
* @author Rob Winch
|
||||
* @since 4.0
|
||||
*/
|
||||
public static final class LogoutRequestBuilder implements RequestBuilder {
|
||||
public static final class LogoutRequestBuilder implements RequestBuilder, Mergeable {
|
||||
private String logoutUrl = "/logout";
|
||||
private RequestPostProcessor postProcessor = csrf();
|
||||
private Mergeable parent;
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
|
||||
MockHttpServletRequest request = post(this.logoutUrl)
|
||||
.accept(MediaType.TEXT_HTML, MediaType.ALL)
|
||||
.buildRequest(servletContext);
|
||||
MockHttpServletRequestBuilder logoutRequest = post(this.logoutUrl)
|
||||
.accept(MediaType.TEXT_HTML, MediaType.ALL);
|
||||
|
||||
if (this.parent != null) {
|
||||
logoutRequest = (MockHttpServletRequestBuilder) logoutRequest.merge(this.parent);
|
||||
}
|
||||
|
||||
MockHttpServletRequest request = logoutRequest.buildRequest(servletContext);
|
||||
logoutRequest.postProcessRequest(request);
|
||||
|
||||
return this.postProcessor.postProcessRequest(request);
|
||||
}
|
||||
|
||||
@@ -122,6 +132,24 @@ public final class SecurityMockMvcRequestBuilders {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMergeEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object merge(Object parent) {
|
||||
if (parent == null) {
|
||||
return this;
|
||||
}
|
||||
if (parent instanceof Mergeable) {
|
||||
this.parent = (Mergeable) parent;
|
||||
return this;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private LogoutRequestBuilder() {
|
||||
}
|
||||
}
|
||||
@@ -132,22 +160,31 @@ public final class SecurityMockMvcRequestBuilders {
|
||||
* @author Rob Winch
|
||||
* @since 4.0
|
||||
*/
|
||||
public static final class FormLoginRequestBuilder implements RequestBuilder {
|
||||
public static final class FormLoginRequestBuilder implements RequestBuilder, Mergeable {
|
||||
private String usernameParam = "username";
|
||||
private String passwordParam = "password";
|
||||
private String username = "user";
|
||||
private String password = "password";
|
||||
private String loginProcessingUrl = "/login";
|
||||
private MediaType acceptMediaType = MediaType.APPLICATION_FORM_URLENCODED;
|
||||
private Mergeable parent;
|
||||
|
||||
private RequestPostProcessor postProcessor = csrf();
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
|
||||
MockHttpServletRequest request = post(this.loginProcessingUrl)
|
||||
.accept(this.acceptMediaType).param(this.usernameParam, this.username)
|
||||
.param(this.passwordParam, this.password)
|
||||
.buildRequest(servletContext);
|
||||
MockHttpServletRequestBuilder loginRequest = post(this.loginProcessingUrl)
|
||||
.accept(this.acceptMediaType)
|
||||
.param(this.usernameParam, this.username)
|
||||
.param(this.passwordParam, this.password);
|
||||
|
||||
if (this.parent != null) {
|
||||
loginRequest = (MockHttpServletRequestBuilder) loginRequest.merge(this.parent);
|
||||
}
|
||||
|
||||
MockHttpServletRequest request = loginRequest.buildRequest(servletContext);
|
||||
loginRequest.postProcessRequest(request);
|
||||
|
||||
return this.postProcessor.postProcessRequest(request);
|
||||
}
|
||||
|
||||
@@ -258,6 +295,24 @@ public final class SecurityMockMvcRequestBuilders {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMergeEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object merge(Object parent) {
|
||||
if (parent == null) {
|
||||
return this;
|
||||
}
|
||||
if (parent instanceof Mergeable ) {
|
||||
this.parent = (Mergeable) parent;
|
||||
return this;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private FormLoginRequestBuilder() {
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -410,7 +410,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
|
||||
private final CsrfTokenRepository delegate;
|
||||
|
||||
private TestCsrfTokenRepository(CsrfTokenRepository delegate) {
|
||||
TestCsrfTokenRepository(CsrfTokenRepository delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
|
||||
+37
-1
@@ -17,14 +17,25 @@ package org.springframework.security.test.web.servlet.request;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.CsrfRequestPostProcessor;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
|
||||
|
||||
public class SecurityMockMvcRequestBuildersFormLoginTests {
|
||||
@@ -82,6 +93,31 @@ public class SecurityMockMvcRequestBuildersFormLoginTests {
|
||||
assertThat(request.getRequestURI()).isEqualTo("/uri-login/val1/val2");
|
||||
}
|
||||
|
||||
/**
|
||||
* spring-restdocs uses postprocessors to do its trick. It will work only if these are merged together
|
||||
* with our request builders. (gh-7572)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void postProcessorsAreMergedDuringMockMvcPerform() throws Exception {
|
||||
RequestPostProcessor postProcessor = mock(RequestPostProcessor.class);
|
||||
when(postProcessor.postProcessRequest(any())).thenAnswer(i -> i.getArgument(0));
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object())
|
||||
.defaultRequest(MockMvcRequestBuilders.get("/").with(postProcessor))
|
||||
.build();
|
||||
|
||||
|
||||
MvcResult mvcResult = mockMvc.perform(formLogin()).andReturn();
|
||||
assertThat(mvcResult.getRequest().getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
assertThat(mvcResult.getRequest().getHeader("Accept"))
|
||||
.isEqualTo(MediaType.toString(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED)));
|
||||
assertThat(mvcResult.getRequest().getParameter("username")).isEqualTo("user");
|
||||
assertThat(mvcResult.getRequest().getParameter("password")).isEqualTo("password");
|
||||
assertThat(mvcResult.getRequest().getRequestURI()).isEqualTo("/login");
|
||||
assertThat(mvcResult.getRequest().getParameter("_csrf")).isNotEmpty();
|
||||
verify(postProcessor).postProcessRequest(any());
|
||||
}
|
||||
|
||||
// gh-3920
|
||||
@Test
|
||||
public void usesAcceptMediaForContentNegotiation() {
|
||||
|
||||
+37
-3
@@ -15,15 +15,28 @@
|
||||
*/
|
||||
package org.springframework.security.test.web.servlet.request;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.CsrfRequestPostProcessor;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
|
||||
|
||||
public class SecurityMockMvcRequestBuildersFormLogoutTests {
|
||||
private MockServletContext servletContext;
|
||||
@@ -71,4 +84,25 @@ public class SecurityMockMvcRequestBuildersFormLogoutTests {
|
||||
assertThat(request.getRequestURI()).isEqualTo("/uri-logout/val1/val2");
|
||||
}
|
||||
|
||||
/**
|
||||
* spring-restdocs uses postprocessors to do its trick. It will work only if these are merged together
|
||||
* with our request builders. (gh-7572)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void postProcessorsAreMergedDuringMockMvcPerform() throws Exception {
|
||||
RequestPostProcessor postProcessor = mock(RequestPostProcessor.class);
|
||||
when(postProcessor.postProcessRequest(any())).thenAnswer(i -> i.getArgument(0));
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object())
|
||||
.defaultRequest(MockMvcRequestBuilders.get("/").with(postProcessor))
|
||||
.build();
|
||||
|
||||
MvcResult mvcResult = mockMvc.perform(logout()).andReturn();
|
||||
assertThat(mvcResult.getRequest().getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
assertThat(mvcResult.getRequest().getHeader("Accept"))
|
||||
.isEqualTo(MediaType.toString(Arrays.asList(MediaType.TEXT_HTML, MediaType.ALL)));
|
||||
assertThat(mvcResult.getRequest().getRequestURI()).isEqualTo("/logout");
|
||||
assertThat(mvcResult.getRequest().getParameter("_csrf")).isNotEmpty();
|
||||
verify(postProcessor).postProcessRequest(any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ import java.util.*;
|
||||
* requests which match the pattern. An example configuration might look like this:
|
||||
*
|
||||
* <pre>
|
||||
* <bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
|
||||
* <bean id="myfilterChainProxy" class="org.springframework.security.web.FilterChainProxy">
|
||||
* <constructor-arg>
|
||||
* <util:list>
|
||||
* <security:filter-chain pattern="/do/not/filter*" filters="none"/>
|
||||
|
||||
+6
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -106,19 +106,16 @@ public class AuthenticationWebFilter implements WebFilter {
|
||||
.filter( matchResult -> matchResult.isMatch())
|
||||
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap( token -> authenticate(exchange, chain, token));
|
||||
.flatMap( token -> authenticate(exchange, chain, token))
|
||||
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
|
||||
.onAuthenticationFailure(new WebFilterExchange(exchange, chain), e));
|
||||
}
|
||||
|
||||
private Mono<Void> authenticate(ServerWebExchange exchange,
|
||||
WebFilterChain chain, Authentication token) {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
|
||||
private Mono<Void> authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) {
|
||||
return this.authenticationManagerResolver.resolve(exchange.getRequest())
|
||||
.flatMap(authenticationManager -> authenticationManager.authenticate(token))
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
|
||||
.flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange))
|
||||
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
|
||||
.onAuthenticationFailure(webFilterExchange, e));
|
||||
.flatMap(authentication -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)));
|
||||
}
|
||||
|
||||
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* A {@link ServerCsrfTokenRepository} that persists the CSRF token in a cookie named "XSRF-TOKEN" and
|
||||
@@ -62,7 +63,7 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
|
||||
|
||||
@Override
|
||||
public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
|
||||
return Mono.fromCallable(this::createCsrfToken);
|
||||
return Mono.fromCallable(this::createCsrfToken).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -60,6 +60,7 @@ import static java.lang.Boolean.TRUE;
|
||||
* </p>
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Parikshit Dutta
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CsrfWebFilter implements WebFilter {
|
||||
@@ -187,7 +188,7 @@ public class CsrfWebFilter implements WebFilter {
|
||||
@Override
|
||||
public Mono<MatchResult> matches(ServerWebExchange exchange) {
|
||||
return Mono.just(exchange.getRequest())
|
||||
.map(r -> r.getMethod())
|
||||
.flatMap(r -> Mono.justOrEmpty(r.getMethod()))
|
||||
.filter(m -> ALLOWED_METHODS.contains(m))
|
||||
.flatMap(m -> MatchResult.notMatch())
|
||||
.switchIfEmpty(MatchResult.match());
|
||||
|
||||
+4
-1
@@ -18,6 +18,7 @@ package org.springframework.security.web.server.csrf;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
@@ -48,7 +49,9 @@ public class WebSessionServerCsrfTokenRepository
|
||||
|
||||
@Override
|
||||
public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
|
||||
return Mono.fromCallable(() -> createCsrfToken());
|
||||
return Mono.just(exchange)
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.fromCallable(() -> createCsrfToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-3
@@ -105,7 +105,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
||||
+ " </head>\n"
|
||||
+ " <body>\n"
|
||||
+ " <div class=\"container\">\n"
|
||||
+ formLogin(queryParams, csrfTokenHtmlInput)
|
||||
+ formLogin(queryParams, contextPath, csrfTokenHtmlInput)
|
||||
+ oauth2LoginLinks(queryParams, contextPath, this.oauth2AuthenticationUrlToClientName)
|
||||
+ " </div>\n"
|
||||
+ " </body>\n"
|
||||
@@ -114,13 +114,13 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
||||
return page.getBytes(Charset.defaultCharset());
|
||||
}
|
||||
|
||||
private String formLogin(MultiValueMap<String, String> queryParams, String csrfTokenHtmlInput) {
|
||||
private String formLogin(MultiValueMap<String, String> queryParams, String contextPath, String csrfTokenHtmlInput) {
|
||||
if (!this.formLoginEnabled) {
|
||||
return "";
|
||||
}
|
||||
boolean isError = queryParams.containsKey("error");
|
||||
boolean isLogoutSuccess = queryParams.containsKey("logout");
|
||||
return " <form class=\"form-signin\" method=\"post\" action=\"/login\">\n"
|
||||
return " <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + "/login\">\n"
|
||||
+ " <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
|
||||
+ createError(isError)
|
||||
+ createLogoutSuccess(isLogoutSuccess)
|
||||
|
||||
+2
-2
@@ -68,7 +68,7 @@ public final class AntPathRequestMatcher
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -77,7 +77,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.
|
||||
*
|
||||
* @param pattern the ant pattern to use for matching
|
||||
|
||||
+16
-1
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,11 +20,14 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -45,6 +48,7 @@ import static org.springframework.web.reactive.function.BodyInserters.fromMultip
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @author Parikshit Dutta
|
||||
* @since 5.0
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@@ -183,6 +187,17 @@ public class CsrfWebFilterTests {
|
||||
chainResult.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-8452
|
||||
public void matchesRequireCsrfProtectionWhenNonStandardHTTPMethodIsUsed() {
|
||||
ServerHttpRequest nonStandardHttpRequest = mock(ServerHttpRequest.class);
|
||||
ServerWebExchange nonStandardHttpExchange = mock(ServerWebExchange.class);
|
||||
when(nonStandardHttpExchange.getRequest()).thenReturn(nonStandardHttpRequest);
|
||||
|
||||
ServerWebExchangeMatcher serverWebExchangeMatcher = CsrfWebFilter.DEFAULT_CSRF_MATCHER;
|
||||
assertThat(serverWebExchangeMatcher.matches(nonStandardHttpExchange).map(MatchResult::isMatch).block()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenSkipExchangeInvokedThenSkips() {
|
||||
PublisherProbe<Void> chainResult = PublisherProbe.empty();
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.web.server.ui;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
public class LoginPageGeneratingWebFilterTests {
|
||||
|
||||
@Test
|
||||
public void filterWhenLoginWithContextPathThenActionContainsContextPath() throws Exception {
|
||||
LoginPageGeneratingWebFilter filter = new LoginPageGeneratingWebFilter();
|
||||
filter.setFormLoginEnabled(true);
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test/login").contextPath("/test"));
|
||||
|
||||
filter.filter(exchange, e -> Mono.empty()).block();
|
||||
|
||||
assertThat(exchange.getResponse().getBodyAsString().block()).contains("action=\"/test/login\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenLoginWithNoContextPathThenActionDoesNotContainsContextPath() throws Exception {
|
||||
LoginPageGeneratingWebFilter filter = new LoginPageGeneratingWebFilter();
|
||||
filter.setFormLoginEnabled(true);
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/login"));
|
||||
|
||||
filter.filter(exchange, e -> Mono.empty()).block();
|
||||
|
||||
assertThat(exchange.getResponse().getBodyAsString().block()).contains("action=\"/login\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user