1
0
mirror of synced 2026-07-08 04:10:03 +00:00

Compare commits

...

98 Commits

Author SHA1 Message Date
Josh Cummings 52814ab387 Release 5.2.7.RELEASE 2020-10-07 10:49:34 -06:00
Eleftheria Stein 9c5ce287d7 Update to Hibernate Validator 6.1.6
Closes gh-9086
2020-10-02 17:13:54 +02:00
Eleftheria Stein cb055f1402 Update to Hibernate Entity manager 5.4.22
Closes gh-9087
2020-10-02 17:13:17 +02:00
Eleftheria Stein 8b3777d0f1 Upgrade to embedded Apache Tomcat 9.0.38
Closes gh-9085
2020-10-02 17:12:37 +02:00
Eleftheria Stein 7181f576fc Update to RSocket 1.0.2
Closes gh-9084
2020-10-02 17:12:06 +02:00
Eleftheria Stein dba3dacc19 Update to Spring Data Moore-SR10
Closes gh-9088
2020-10-02 17:11:44 +02:00
Eleftheria Stein 5d0eb32f4d Update to Spring Framework 5.2.9
Closes gh-9083
2020-10-02 17:10:10 +02:00
Eleftheria Stein 984fdd4c00 Update to Reactor Dysprosium-SR12
Closes gh-9082
2020-10-02 17:09:40 +02:00
Eleftheria Stein 58b68625bb Update to Spring Boot 2.2.10
Closes gh-9081
2020-10-02 17:08:58 +02:00
Eleftheria Stein 6a87ccca4a Update to GAE 1.9.82
Closes gh-9080
2020-10-02 14:34:12 +02:00
Eleftheria Stein 925a6685d9 Update to org.aspectj 1.9.6
Closes gh-9079
2020-10-02 14:30:11 +02:00
Malyshau Stanislau bd3e3ed2c5 Add try-with-resources to close stream
Closes gh-9041
2020-09-29 08:46:18 -06:00
Artem Grankin 6dad918e7b Replace expired msdn link with latest web archive copy
Initial link expired in March, 2016. Latest copy found in web archive is from February, 2016
2020-09-28 17:19:42 -06:00
ilee 89ad42a06f Update ssl setup guide link in tomcat server 2020-09-24 13:53:48 -06:00
Tomoki Tsubaki 85889d5e0b Create the CSRF token on the bounded elactic scheduler
The CSRF token is generated by UUID.randomUUID() which is I/O blocking operation.
This commit changes the subscriber thread to the bounded elactic scheduler.

Closes gh-9018
2020-09-16 09:21:11 -06:00
Eleftheria Stein 8c7dbf2e3d Next development version 2020-08-05 17:16:34 +02:00
Eleftheria Stein 2443675a59 Release 5.2.6.RELEASE 2020-08-05 15:34:14 +02:00
Eleftheria Stein 04d6d5ae74 Update to nohttp 0.0.5.RELEASE
Closes gh-8927
2020-08-05 13:47:12 +02:00
Eleftheria Stein acbbb6941c Update to groovy 2.4.20
Closes gh-8915
2020-08-05 11:25:05 +02:00
Eleftheria Stein 4a1b96d51a Update to embedded Tomcat websocket 8.5.57
Closes gh-8914
2020-08-05 11:24:47 +02:00
Eleftheria Stein 9fbed06b6b Update blockhound to 1.0.4.RELEASE
Closes gh-8916
2020-08-05 11:24:16 +02:00
Eleftheria Stein fea90f9328 Update to PowerMock Mockito2 2.0.7
Closes gh-8917
2020-08-05 11:23:56 +02:00
Eleftheria Stein 22a1bce83c Update to Spring Data Moore-SR9
Closes gh-8918
2020-08-05 11:23:21 +02:00
Eleftheria Stein 8dfe0fa815 Update to Spring Framework 5.2.8.RELEASE
Closes gh-8919
2020-08-05 11:22:53 +02:00
Eleftheria Stein b408f074d5 Update to Reactor Dysprosium-SR10
Closes gh-8920
2020-08-05 11:22:31 +02:00
Eleftheria Stein 669fdba98c Update to Spring Boot 2.2.9.RELEASE
Closes gh-8921
2020-08-05 11:21:45 +02:00
Eleftheria Stein b4d2217d45 Update to jaxb-impl 2.3.3
Closes gh-8912
2020-08-05 11:06:50 +02:00
Eleftheria Stein b1e09eddaa Update to Jackson 2.10.5
Closes gh-8910
2020-08-05 11:04:25 +02:00
Eleftheria Stein b425b622db Update to GAE 1.9.81
Closes gh-8911
2020-08-05 10:41:59 +02:00
Eleftheria Stein 6512c04079 Upgrade to embedded Apache Tomcat 9.0.37
Closes gh-8913
2020-08-05 10:37:32 +02:00
Dávid Kováč da4bd22c6d Resolve Bearer token after subscribing to publisher
Bearer token was resolved immediately after calling method convert. In situations when malformed token was provided or authorization header and access token query param were present in request exception was thrown instead of signalling error.
After this change Bearer token is resolved on subscription and invalid states are handled by signaling error to subscriber.

Closes gh-8865
2020-08-03 11:11:09 -05:00
Josh Cummings fd669f751d Remove unused import
Issue gh-8589
2020-07-31 08:45:47 -06:00
Josh Cummings 510d1b8121 Polish to Avoid NPE
Issue gh-5648

Co-authored-by: MattyA <mat.auburn@gmail.com>
2020-07-30 17:59:31 -06:00
Josh Cummings 2f80b8a5be Additional Jwt Validation Debug Messages
Closes gh-8589

Co-authored-by: MattyA <mat.auburn@gmail.com>
2020-07-30 17:58:16 -06:00
Josh Cummings 0efdb2c92c Polish WebSecurityConfigurerAdapter JavaDoc
Issue gh-8784
2020-07-20 15:24:23 -06:00
Romil Patel 79d8b616f0 WebSecurityConfigurerAdapter JavaDoc
Closes gh-8784
2020-07-20 15:24:19 -06:00
Josh Cummings 9d8920f1b1 Polish Bearer Token Padding
Issue gh-8502
2020-07-16 12:22:45 -06:00
kothasa 6519029340 Bearer Token Padding
Closes gh-8502
2020-07-16 12:22:32 -06:00
wangsong 3dc59686fa Fix ProviderManager Javadoc typo
Closes gh-8800
2020-07-07 17:17:16 -05:00
Rob Winch dcd2137418 LoginPageGeneratingWebFilter honors context path
Closes gh-8807
2020-07-07 13:37:06 -05:00
Ellie Bahadori 1bb49bbc2e Use Github Actions workflow for PRs and remove Travis
Closes gh-8718
2020-06-30 05:05:48 -04:00
Rob Winch 5f395a3513 Better scp Retry Settings 2020-06-25 11:34:46 -05:00
Rob Winch 0356af03e6 Revert "Update to spring-build-conventions:0.0.33.RELEASE"
This reverts commit 5343325091.
2020-06-25 11:32:18 -05:00
Rob Winch 5343325091 Update to spring-build-conventions:0.0.33.RELEASE
Closes gh-8759
2020-06-25 11:30:12 -05:00
Dávid Kovács fa9898dd6d formLogin() and login() implement Mergable
This is necessary so that default requests like Spring REST Docs work.

Closes gh-7572
2020-06-22 14:58:39 -05:00
Evgeniy Cheban bff6d82dd0 DefaultWebSecurityExpressionHandler uses RoleHierarchy bean
Fixes gh-7059
2020-06-10 17:08:50 -04:00
Joe Grandja 674e2c0a8e OAuth2LoginAuthenticationWebFilter should handle OAuth2AuthorizationException
Issue gh-8609
2020-06-09 16:24:00 -04:00
Joe Grandja 11c1236261 OAuth2AuthorizationCodeGrantWebFilter should handle OAuth2AuthorizationException
Fixes gh-8609
2020-06-09 16:24:00 -04:00
Eleftheria Stein 98467755ad Update to RSocket 1.0.1
Fixes gh-8664
2020-06-08 17:18:33 -04:00
Eleftheria Stein 0e37c722e2 Revert "Temporarily ignore RSocket integration tests"
This reverts commit d5eeec0ae6.

Fixes gh-8643
2020-06-08 16:14:34 -04:00
Rob Winch c71a893e08 Add subscriberContext to PayloadSocketAcceptor delegate.accept
Closes gh-8654
2020-06-05 12:55:07 -05:00
Eleftheria Stein 69ff2ab3fc Next development version 2020-06-03 16:17:30 -04:00
Eleftheria Stein 38a731d5e4 Release 5.2.5.RELEASE 2020-06-03 16:02:37 -04:00
Eleftheria Stein d5eeec0ae6 Temporarily ignore RSocket integration tests
Issue gh-8643
2020-06-03 15:29:59 -04:00
Eleftheria Stein fd7fe482f5 Revert "Release 5.2.5.RELEASE"
This reverts commit b88e094f37.
2020-06-03 15:29:44 -04:00
Eleftheria Stein b88e094f37 Release 5.2.5.RELEASE 2020-06-03 10:59:04 -04:00
Eleftheria Stein 04453cec96 Update to groovy 2.4.19
Fixes gh-8525
2020-06-02 17:18:21 -04:00
Eleftheria Stein d9919b838d Update to okhttp 3.14.9
Fixes gh-8629
2020-06-02 17:17:56 -04:00
Eleftheria Stein 05b9591e44 Update to Jython 2.5.3
Fixes gh-8628
2020-06-02 17:17:29 -04:00
Eleftheria Stein 1b93b0c9a6 Update to mockwebserver 3.14.9
Fixes gh-8627
2020-06-02 17:17:05 -04:00
Eleftheria Stein e87783024c Update to RSocket 1.0.0
Fixes gh-8626
2020-06-02 17:15:05 -04:00
Eleftheria Stein 7e9c7534f5 Update to Spring Boot 2.2.7
Fixes gh-8630
2020-06-02 17:14:44 -04:00
Rob Winch 24fd9579c5 Delay AuthenticationPrincipalArgumentResolver Creation
Use ObjectProvider<AuthenticationPrincipalArgumentResolver> to delay its
lookup.

Closes gh-8613
2020-05-29 16:51:47 -05:00
Eleftheria Stein a6dd119266 Mock request with non-standard HTTP method in test
Fixes gh-8594
2020-05-26 11:32:27 -04:00
justmehyp 774ea6980b Remove unused field 'digester' in Md4PasswordEncoder
`private Digester digester;`  defined in Md4PasswordEncoder is never used. So remove it.

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

Remove from AccessControlEntryImpl.hashCode method invocation of
acl.hashCode

fixes gh-5401
2020-05-21 10:07:54 -05:00
Dávid Kovács eaaee899fc Object ID Identicy conversion to long fails on old schema
This change fixed a bug which tried to convert non-string object as string

Fixes gh-7621
2020-05-19 13:45:23 -05:00
cbornet 21c1d98f64 Create the CSRF token on the bounded elactic scheduler
The CSRF token is created with a call to UUID.randomUUID which is blocking.
This change ensures this blocking call is done on the bounded elastic scheduler which supports blocking calls.

Fixes gh-8128
2020-05-18 11:06:45 -05:00
Artyom Tarynin 9f33ce312a Update AntPathRequestMatcher.java
Fixes gh-8512
2020-05-14 10:05:52 -04:00
Dávid Kovács e382c269ef Document NoOpPasswordEncoder will not be removed
This commit adds extension to deprecation notice.

Fixes gh-8506
2020-05-13 12:56:04 -05:00
Rob Winch ce7c501f9c AbstractUserDetailsReactiveAuthenticationManager uses boundidElastic()
Some JVMs have blocking operations when accessing SecureRandom and thus
this needs to be performed in a pool that is larger than the number of
CPUs

Closes gh-7522
2020-05-12 13:24:47 -05:00
Rob Winch bd93616567 Fix non-standard HTTP method for CsrfWebFilter
Closes gh-8452
2020-05-12 13:18:01 -05:00
Eleftheria Stein f9872d3dee Next Development Version 2020-05-06 15:59:24 -04:00
Eleftheria Stein b7212bd975 Release 5.2.4.RELEASE 2020-05-06 15:16:32 -04:00
Eleftheria Stein 50da82d88d Temporarily build against Framework 5.2.x snapshot
Issue: gh-8489
2020-05-06 14:43:53 -04:00
Eleftheria Stein 3ed31400f7 Update SAML2 errors in integration tests 2020-05-06 14:16:41 -04:00
Josh Cummings 69b1bc62ff Polish OpenSamlAuthenticationProvider
- Use type-safe CriteriaSet
- Keep Assertion immutable

Closes gh-8471
2020-05-06 14:15:45 -04:00
Eleftheria Stein f43d00fac6 Update to Byte Buddy 1.9.16
Closes gh-8481
2020-05-05 17:50:07 -04:00
Eleftheria Stein 01cce49ee5 Upgrade to embedded Apache Tomcat 9.0.34
Closes gh-8469
2020-05-05 17:48:03 -04:00
Eleftheria Stein 6ccc18fde2 Update to Jackson 2.10.4
Closes gh-8466
2020-05-05 17:47:08 -04:00
Eleftheria Stein d34f7a8474 Update to org.powermock 2.0.7
Closes gh-8465
2020-05-05 17:46:12 -04:00
Eleftheria Stein ba78fae246 Update RSocket to 1.0.0-RC7
Closes gh-8468
2020-05-05 17:45:37 -04:00
Eleftheria Stein 98922b001a Update to Spring Data Moore-SR7
Closes gh-8462
2020-05-05 17:44:33 -04:00
Eleftheria Stein 19a4386e49 Update to Spring Framework 5.2.6.RELEASE
Closes gh-8463
2020-05-05 17:43:43 -04:00
Eleftheria Stein 4fa67a1d9b Update to Reactor Dysprosium-SR7
Closes gh-8464
2020-05-05 17:43:00 -04:00
Eleftheria Stein 94fd7e3a2b Update to GAE 1.9.80
Closes gh-8467
2020-05-05 17:41:43 -04:00
Eleftheria Stein e9e736d1e6 Clean up Javadoc
Fixes gh-8480
2020-05-05 17:34:14 -04:00
Dávid Kovács c399185365 Add ROLE_INFRASTRUCTURE to infrastructure beans
Closes gh-8407
2020-04-27 09:02:54 -05:00
Dávid Kovács 93a1fc104c ActiveDirectoryLdapAuthenticationProvider uses InternalAuthenticationServiceException
Closes gh-2884
2020-04-24 10:20:35 -05:00
Julian Müller 9dd68f86d3 Enables empty authorityPrefix
- docs stated that empty authorityPrefix are allowed but implementation denied to use `""`
- commit removes the `hasText`-limitation but restricts to `notNull`

Fixes gh-8421
2020-04-22 08:55:54 -05:00
Souphorn 01f904895e Fix typo with correct capitalization
Closes gh-8406
2020-04-17 12:13:32 -04:00
Josh Cummings 067cb4579e Polish OpenSamlAuthenticationProviderTests
- Add missing assertion

Issue gh-6019
2020-04-17 09:29:39 -06:00
Rob Winch 7b34b223e6 Logout defaults to use Global SecurityContextServerLogoutHandler
Closes gh-8375
2020-04-13 16:38:05 -05:00
Rob Winch 52ed597f4c Fix example in javadoc of FilterChainProxy
Closes gh-8344
2020-04-08 09:14:02 -05:00
Alan Czajkowski 564d5644c9 BCryptPasswordEncoder rawPassword cannot be null
Closes gh-8317
2020-04-07 13:46:13 -05:00
hotire f011c36ba4 Fix typo in Javadoc of ServerHttpSecurity#hasAuthority
Closes gh-8336
2020-04-06 14:20:19 -05:00
Eleftheria Stein bb654fdcdf Fix HttpSecurity Javadoc
Fixes gh-4404
2020-04-02 11:41:15 -04:00
Eleftheria Stein 30a63026f8 Next Development Version 2020-04-01 14:26:04 -04:00
77 changed files with 1207 additions and 478 deletions
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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'
-2
View File
@@ -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
@@ -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
View File
@@ -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 {
@@ -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);
@@ -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
@@ -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();
}
@@ -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) {
@@ -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>
*
@@ -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));
@@ -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
*/
@@ -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>
*
@@ -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());
}
};
}
@@ -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) {
@@ -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();
@@ -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;
}
}
@@ -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();
}
}
@@ -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
@@ -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
@@ -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));
@@ -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;
@@ -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 {
@@ -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
View File
@@ -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
+31 -31
View File
@@ -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'
@@ -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
@@ -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);
}
}
@@ -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(
@@ -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(),
@@ -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 &quot;exchange&quot; 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);
}
}
}
@@ -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)
@@ -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
@@ -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
@@ -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<>();
@@ -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);
}
}
@@ -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
@@ -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());
}
@@ -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;
@@ -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);
@@ -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);
@@ -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;
}
@@ -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;
@@ -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();
@@ -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();
@@ -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();
@@ -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
@@ -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)
);
}
@@ -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;
}
}
@@ -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());
@@ -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);
}
}
@@ -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);
@@ -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"
)
)
);
@@ -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() {
}
}
@@ -410,7 +410,7 @@ public final class SecurityMockMvcRequestPostProcessors {
private final CsrfTokenRepository delegate;
private TestCsrfTokenRepository(CsrfTokenRepository delegate) {
TestCsrfTokenRepository(CsrfTokenRepository delegate) {
this.delegate = delegate;
}
@@ -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() {
@@ -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>
* &lt;bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy"&gt;
* &lt;bean id="myfilterChainProxy" class="org.springframework.security.web.FilterChainProxy"&gt;
* &lt;constructor-arg&gt;
* &lt;util:list&gt;
* &lt;security:filter-chain pattern="/do/not/filter*" filters="none"/&gt;
@@ -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) {
@@ -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());
@@ -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
@@ -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)
@@ -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
@@ -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();
@@ -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\"");
}
}