Compare commits
185 Commits
5.6.3
...
5.2.7.RELEASE
| Author | SHA1 | Date | |
|---|---|---|---|
| 52814ab387 | |||
| 9c5ce287d7 | |||
| cb055f1402 | |||
| 8b3777d0f1 | |||
| 7181f576fc | |||
| dba3dacc19 | |||
| 5d0eb32f4d | |||
| 984fdd4c00 | |||
| 58b68625bb | |||
| 6a87ccca4a | |||
| 925a6685d9 | |||
| bd3e3ed2c5 | |||
| 6dad918e7b | |||
| 89ad42a06f | |||
| 85889d5e0b | |||
| 8c7dbf2e3d | |||
| 2443675a59 | |||
| 04d6d5ae74 | |||
| acbbb6941c | |||
| 4a1b96d51a | |||
| 9fbed06b6b | |||
| fea90f9328 | |||
| 22a1bce83c | |||
| 8dfe0fa815 | |||
| b408f074d5 | |||
| 669fdba98c | |||
| b4d2217d45 | |||
| b1e09eddaa | |||
| b425b622db | |||
| 6512c04079 | |||
| da4bd22c6d | |||
| fd669f751d | |||
| 510d1b8121 | |||
| 2f80b8a5be | |||
| 0efdb2c92c | |||
| 79d8b616f0 | |||
| 9d8920f1b1 | |||
| 6519029340 | |||
| 3dc59686fa | |||
| dcd2137418 | |||
| 1bb49bbc2e | |||
| 5f395a3513 | |||
| 0356af03e6 | |||
| 5343325091 | |||
| fa9898dd6d | |||
| bff6d82dd0 | |||
| 674e2c0a8e | |||
| 11c1236261 | |||
| 98467755ad | |||
| 0e37c722e2 | |||
| c71a893e08 | |||
| 69ff2ab3fc | |||
| 38a731d5e4 | |||
| d5eeec0ae6 | |||
| fd7fe482f5 | |||
| b88e094f37 | |||
| 04453cec96 | |||
| d9919b838d | |||
| 05b9591e44 | |||
| 1b93b0c9a6 | |||
| e87783024c | |||
| 7e9c7534f5 | |||
| 24fd9579c5 | |||
| a6dd119266 | |||
| 774ea6980b | |||
| 279ddbe223 | |||
| eaaee899fc | |||
| 21c1d98f64 | |||
| 9f33ce312a | |||
| e382c269ef | |||
| ce7c501f9c | |||
| bd93616567 | |||
| f9872d3dee | |||
| b7212bd975 | |||
| 50da82d88d | |||
| 3ed31400f7 | |||
| 69b1bc62ff | |||
| f43d00fac6 | |||
| 01cce49ee5 | |||
| 6ccc18fde2 | |||
| d34f7a8474 | |||
| ba78fae246 | |||
| 98922b001a | |||
| 19a4386e49 | |||
| 4fa67a1d9b | |||
| 94fd7e3a2b | |||
| e9e736d1e6 | |||
| c399185365 | |||
| 93a1fc104c | |||
| 9dd68f86d3 | |||
| 01f904895e | |||
| 067cb4579e | |||
| 7b34b223e6 | |||
| 52ed597f4c | |||
| 564d5644c9 | |||
| f011c36ba4 | |||
| bb654fdcdf | |||
| 30a63026f8 | |||
| f93e5afe16 | |||
| 7b5bdac74d | |||
| 80a16e828f | |||
| ed133d4b8d | |||
| 433646b551 | |||
| dd6974812f | |||
| 97f1e368ee | |||
| 7018e7ec51 | |||
| 0313757846 | |||
| a021cd3fd5 | |||
| a57f4bb025 | |||
| a02cd9d9e3 | |||
| eacede6171 | |||
| 08b635a85a | |||
| e3c3601cf6 | |||
| 1ca3ea3a71 | |||
| dbdf02e864 | |||
| 774dd7bb13 | |||
| 6a9585dc0d | |||
| 298d40b7a5 | |||
| 615f9a3f05 | |||
| 32c3353921 | |||
| 401597c673 | |||
| 258627eaee | |||
| 01f8eb3961 | |||
| a9a9c2c0fd | |||
| cb7786bf97 | |||
| 4706b16a2b | |||
| 98bd1a3f60 | |||
| f06aa724bf | |||
| 512ad9e7e4 | |||
| 256aba7b37 | |||
| 86e25ff2ab | |||
| a49a325db2 | |||
| 75f22285c6 | |||
| 8fa16ce63e | |||
| 32c02fbedb | |||
| 96ff3a54a9 | |||
| 9092115b8a | |||
| 3dbfef9ef1 | |||
| 8acdb82e6a | |||
| 5ce0ce3f38 | |||
| 6141132cfa | |||
| cc7ea4acd3 | |||
| 1e4736f9b3 | |||
| 0012e24c46 | |||
| 2dc8147106 | |||
| 1da8e9df13 | |||
| 9a2b71d931 | |||
| c4ccc96655 | |||
| 6c310213a8 | |||
| a5b6b9a398 | |||
| 9e6910273c | |||
| ea809b01a6 | |||
| 8054239a12 | |||
| 46486194c2 | |||
| 00b08bc725 | |||
| 6e0fbfcccd | |||
| 87ea083520 | |||
| 9db3f51f2a | |||
| 3cc4a945c6 | |||
| dbc43fb47d | |||
| ce6a0368bd | |||
| 9dd3dfe718 | |||
| edb6cd3729 | |||
| 2dbedf7af5 | |||
| 630eb10704 | |||
| f4d4c08329 | |||
| cc956a66df | |||
| 29182abb34 | |||
| b754a3d635 | |||
| 0d24e2b8cf | |||
| b00999deed | |||
| 59ca2ddf65 | |||
| 0782228914 | |||
| 29eb8b9177 | |||
| bd6ff1f319 | |||
| 6db7b457b7 | |||
| 840d3aa986 | |||
| 4c5c4f6cce | |||
| 148b570a98 | |||
| 752d5f29aa | |||
| e4aa3be4c5 | |||
| 0babe7d930 | |||
| b905cb8aaa | |||
| 19c2209a12 | |||
| 18f48e4a16 |
@@ -0,0 +1,22 @@
|
||||
name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '8'
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
language: java
|
||||
|
||||
jdk:
|
||||
- openjdk8
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
|
||||
script: ./gradlew build --refresh-dependencies --no-daemon --continue
|
||||
Vendored
+1
-1
@@ -56,7 +56,7 @@ try {
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
|
||||
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.2.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: snapshots'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
|
||||
|
||||
image:https://travis-ci.org/spring-projects/spring-security.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/spring-security"]
|
||||
|
||||
= Spring Security
|
||||
|
||||
Spring Security provides security services for the https://docs.spring.io[Spring IO Platform]. Spring Security 5.0 requires Spring 5.0 as
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -131,10 +131,9 @@ public class AccessControlEntryImpl implements AccessControlEntry,
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.acl.hashCode();
|
||||
result = 31 * result + this.permission.hashCode();
|
||||
int result = this.permission.hashCode();
|
||||
result = 31 * result + (this.id != null ? this.id.hashCode() : 0);
|
||||
result = 31 * result + this.sid.hashCode();
|
||||
result = 31 * result + (this.sid.hashCode());
|
||||
result = 31 * result + (this.auditFailure ? 1 : 0);
|
||||
result = 31 * result + (this.auditSuccess ? 1 : 0);
|
||||
result = 31 * result + (this.granting ? 1 : 0);
|
||||
|
||||
@@ -118,7 +118,7 @@ class AclClassIdUtils {
|
||||
*/
|
||||
private Long convertToLong(Serializable identifier) {
|
||||
Long idAsLong;
|
||||
if (canConvertFromStringTo(Long.class)) {
|
||||
if (conversionService.canConvert(identifier.getClass(), Long.class)) {
|
||||
idAsLong = conversionService.convert(identifier, Long.class);
|
||||
} else {
|
||||
idAsLong = Long.valueOf(identifier.toString());
|
||||
|
||||
@@ -577,6 +577,25 @@ public class AclImplTests {
|
||||
assertThat(acl.isGranted(permissions, sids, false)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCodeWithoutStackOverFlow() throws Exception {
|
||||
//given
|
||||
Sid sid = new PrincipalSid("pSid");
|
||||
ObjectIdentity oid = new ObjectIdentityImpl("type", 1);
|
||||
AclAuthorizationStrategy authStrategy = new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("role"));
|
||||
PermissionGrantingStrategy grantingStrategy = new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
|
||||
|
||||
AclImpl acl = new AclImpl(oid, 1L, authStrategy, grantingStrategy, null, null, false, sid);
|
||||
AccessControlEntryImpl ace = new AccessControlEntryImpl(1L, acl, sid, BasePermission.READ, true, true, true);
|
||||
|
||||
Field fieldAces = FieldUtils.getField(AclImpl.class, "aces");
|
||||
fieldAces.setAccessible(true);
|
||||
List<AccessControlEntryImpl> aces = (List<AccessControlEntryImpl>) fieldAces.get(acl);
|
||||
aces.add(ace);
|
||||
//when - then none StackOverFlowError been raised
|
||||
ace.hashCode();
|
||||
}
|
||||
|
||||
// ~ Inner Classes
|
||||
// ==================================================================================================
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
@@ -39,6 +40,7 @@ import static org.mockito.BDDMockito.given;
|
||||
public class AclClassIdUtilsTest {
|
||||
|
||||
private static final Long DEFAULT_IDENTIFIER = 999L;
|
||||
private static final BigInteger BIGINT_IDENTIFIER = new BigInteger("999");
|
||||
private static final String DEFAULT_IDENTIFIER_AS_STRING = DEFAULT_IDENTIFIER.toString();
|
||||
|
||||
@Mock
|
||||
@@ -62,6 +64,15 @@ public class AclClassIdUtilsTest {
|
||||
assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnLongIfIdentifierIsBigInteger() throws SQLException {
|
||||
// when
|
||||
Serializable newIdentifier = aclClassIdUtils.identifierFrom(BIGINT_IDENTIFIER, resultSet);
|
||||
|
||||
// then
|
||||
assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnLongIfClassIdTypeIsNull() throws SQLException {
|
||||
// given
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.23.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.2.RELEASE'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
|
||||
classpath "io.freefair.gradle:aspectj-plugin:4.0.2"
|
||||
}
|
||||
repositories {
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public class ServiceAuthenticationDetailsSource implements
|
||||
// ===================================================================================================
|
||||
|
||||
/**
|
||||
* Creates an implementation that uses the specified ServiceProperites and the default
|
||||
* Creates an implementation that uses the specified ServiceProperties and the default
|
||||
* CAS artifactParameterName.
|
||||
*
|
||||
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
|
||||
|
||||
+4
@@ -16,8 +16,10 @@
|
||||
package org.springframework.security.config.annotation.configuration;
|
||||
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -34,9 +36,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
* @since 3.2
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class ObjectPostProcessorConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public ObjectPostProcessor<Object> objectPostProcessor(
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
return new AutowireBeanFactoryObjectPostProcessor(beanFactory);
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.*;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
@@ -80,6 +81,7 @@ import org.springframework.util.Assert;
|
||||
* @see EnableGlobalMethodSecurity
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class GlobalMethodSecurityConfiguration
|
||||
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware {
|
||||
private static final Log logger = LogFactory
|
||||
|
||||
+4
@@ -15,14 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
class Jsr250MetadataSourceConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() {
|
||||
return new Jsr250MethodSecurityMetadataSource();
|
||||
}
|
||||
|
||||
+2
@@ -54,6 +54,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public DelegatingMethodSecurityMetadataSource methodMetadataSource(MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
|
||||
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
|
||||
methodSecurityExpressionHandler);
|
||||
@@ -74,6 +75,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
|
||||
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
||||
if (this.grantedAuthorityDefaults != null) {
|
||||
|
||||
+2
-1
@@ -1105,6 +1105,7 @@ public final class HttpSecurity extends
|
||||
|
||||
/**
|
||||
* Allows restricting access based upon the {@link HttpServletRequest} using
|
||||
* {@link RequestMatcher} implementations (i.e. via URL patterns).
|
||||
*
|
||||
* <h2>Example Configurations</h2>
|
||||
*
|
||||
@@ -1471,7 +1472,7 @@ public final class HttpSecurity extends
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return the {@link ServletApiConfigurer} for further customizations
|
||||
* @return the {@link CsrfConfigurer} for further customizations
|
||||
* @throws Exception
|
||||
*/
|
||||
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,6 +31,7 @@ import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.SecurityBuilder;
|
||||
@@ -74,6 +75,7 @@ import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
* @see WebSecurityConfiguration
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Evgeniy Cheban
|
||||
* @since 3.2
|
||||
*/
|
||||
public final class WebSecurity extends
|
||||
@@ -383,6 +385,11 @@ public final class WebSecurity extends
|
||||
throws BeansException {
|
||||
this.defaultWebSecurityExpressionHandler
|
||||
.setApplicationContext(applicationContext);
|
||||
|
||||
try {
|
||||
this.defaultWebSecurityExpressionHandler.setRoleHierarchy(applicationContext.getBean(RoleHierarchy.class));
|
||||
} catch (NoSuchBeanDefinitionException e) {}
|
||||
|
||||
try {
|
||||
this.defaultWebSecurityExpressionHandler.setPermissionEvaluator(applicationContext.getBean(
|
||||
PermissionEvaluator.class));
|
||||
|
||||
+10
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import reactor.core.CoreSubscriber;
|
||||
@@ -92,32 +93,21 @@ class SecurityReactorContextConfiguration {
|
||||
}
|
||||
|
||||
private static boolean contextAttributesAvailable() {
|
||||
HttpServletRequest servletRequest = null;
|
||||
HttpServletResponse servletResponse = null;
|
||||
ServletRequestAttributes requestAttributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes != null) {
|
||||
servletRequest = requestAttributes.getRequest();
|
||||
servletResponse = requestAttributes.getResponse();
|
||||
}
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null || servletRequest != null || servletResponse != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return SecurityContextHolder.getContext().getAuthentication() != null ||
|
||||
RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes;
|
||||
}
|
||||
|
||||
private static Map<Object, Object> getContextAttributes() {
|
||||
HttpServletRequest servletRequest = null;
|
||||
HttpServletResponse servletResponse = null;
|
||||
ServletRequestAttributes requestAttributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes != null) {
|
||||
servletRequest = requestAttributes.getRequest();
|
||||
servletResponse = requestAttributes.getResponse();
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes instanceof ServletRequestAttributes) {
|
||||
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
|
||||
servletRequest = servletRequestAttributes.getRequest();
|
||||
servletResponse = servletRequestAttributes.getResponse(); // possible null
|
||||
}
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null && servletRequest == null && servletResponse == null) {
|
||||
if (authentication == null && servletRequest == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -330,6 +330,13 @@ public abstract class WebSecurityConfigurerAdapter implements
|
||||
/**
|
||||
* Override this method to configure {@link WebSecurity}. For example, if you wish to
|
||||
* ignore certain requests.
|
||||
*
|
||||
* Endpoints specified in this method will be ignored by Spring Security, meaning it
|
||||
* will not protect them from CSRF, XSS, Clickjacking, and so on.
|
||||
*
|
||||
* Instead, if you want to protect endpoints against common vulnerabilities, then see
|
||||
* {@link #configure(HttpSecurity)} and the {@link HttpSecurity#authorizeRequests}
|
||||
* configuration method.
|
||||
*/
|
||||
public void configure(WebSecurity web) throws Exception {
|
||||
}
|
||||
@@ -343,6 +350,10 @@ public abstract class WebSecurityConfigurerAdapter implements
|
||||
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
|
||||
* </pre>
|
||||
*
|
||||
* Any endpoint that requires defense against common vulnerabilities can be specified here, including public ones.
|
||||
* See {@link HttpSecurity#authorizeRequests} and the `permitAll()` authorization rule
|
||||
* for more details on public endpoints.
|
||||
*
|
||||
* @param http the {@link HttpSecurity} to modify
|
||||
* @throws Exception if an error occurs
|
||||
*/
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
|
||||
|
||||
/**
|
||||
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
|
||||
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites
|
||||
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
|
||||
* set.
|
||||
*
|
||||
* @param successHandler the {@link AuthenticationSuccessHandler}.
|
||||
|
||||
+2
-2
@@ -183,7 +183,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
*
|
||||
* <p>
|
||||
* Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
|
||||
* "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* "https://web.archive.org/web/20160201174302/https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* >X-XSS-Protection header</a>
|
||||
* </p>
|
||||
*
|
||||
@@ -198,7 +198,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
*
|
||||
* <p>
|
||||
* Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
|
||||
* "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* "https://web.archive.org/web/20160201174302/https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* >X-XSS-Protection header</a>
|
||||
* </p>
|
||||
*
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
|
||||
matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
|
||||
matchers.add(notXRequestedWith);
|
||||
matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
|
||||
matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM));
|
||||
|
||||
return new AndRequestMatcher(matchers);
|
||||
}
|
||||
|
||||
+36
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -222,9 +222,16 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
|
||||
@Override
|
||||
public void init(H http) {
|
||||
validateConfiguration();
|
||||
|
||||
registerDefaultAccessDeniedHandler(http);
|
||||
registerDefaultEntryPoint(http);
|
||||
registerDefaultCsrfOverride(http);
|
||||
|
||||
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
|
||||
if (authenticationProvider != null) {
|
||||
http.authenticationProvider(authenticationProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,8 +239,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
BearerTokenResolver bearerTokenResolver = getBearerTokenResolver();
|
||||
this.requestMatcher.setBearerTokenResolver(bearerTokenResolver);
|
||||
|
||||
validateConfiguration();
|
||||
|
||||
AuthenticationManagerResolver resolver = this.authenticationManagerResolver;
|
||||
if (resolver == null) {
|
||||
AuthenticationManager authenticationManager = getAuthenticationManager(http);
|
||||
@@ -321,9 +326,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
return this.decoder;
|
||||
}
|
||||
|
||||
AuthenticationManager getAuthenticationManager(H http) {
|
||||
AuthenticationProvider getAuthenticationProvider() {
|
||||
if (this.authenticationManager != null) {
|
||||
return this.authenticationManager;
|
||||
return null;
|
||||
}
|
||||
|
||||
JwtDecoder decoder = getJwtDecoder();
|
||||
@@ -333,9 +338,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
JwtAuthenticationProvider provider =
|
||||
new JwtAuthenticationProvider(decoder);
|
||||
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
|
||||
AuthenticationProvider authenticationProvider = postProcess(provider);
|
||||
return postProcess(provider);
|
||||
}
|
||||
|
||||
http.authenticationProvider(authenticationProvider);
|
||||
AuthenticationManager getAuthenticationManager(H http) {
|
||||
if (this.authenticationManager != null) {
|
||||
return this.authenticationManager;
|
||||
}
|
||||
|
||||
return http.getSharedObject(AuthenticationManager.class);
|
||||
}
|
||||
@@ -391,16 +400,19 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
return this.context.getBean(OpaqueTokenIntrospector.class);
|
||||
}
|
||||
|
||||
AuthenticationProvider getAuthenticationProvider() {
|
||||
if (this.authenticationManager != null) {
|
||||
return null;
|
||||
}
|
||||
OpaqueTokenIntrospector introspector = getIntrospector();
|
||||
return new OpaqueTokenAuthenticationProvider(introspector);
|
||||
}
|
||||
|
||||
AuthenticationManager getAuthenticationManager(H http) {
|
||||
if (this.authenticationManager != null) {
|
||||
return this.authenticationManager;
|
||||
}
|
||||
|
||||
OpaqueTokenIntrospector introspector = getIntrospector();
|
||||
OpaqueTokenAuthenticationProvider provider =
|
||||
new OpaqueTokenAuthenticationProvider(introspector);
|
||||
http.authenticationProvider(provider);
|
||||
|
||||
return http.getSharedObject(AuthenticationManager.class);
|
||||
}
|
||||
}
|
||||
@@ -439,6 +451,18 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
csrf.ignoringRequestMatchers(this.requestMatcher);
|
||||
}
|
||||
|
||||
AuthenticationProvider getAuthenticationProvider() {
|
||||
if (this.jwtConfigurer != null) {
|
||||
return this.jwtConfigurer.getAuthenticationProvider();
|
||||
}
|
||||
|
||||
if (this.opaqueTokenConfigurer != null) {
|
||||
return this.opaqueTokenConfigurer.getAuthenticationProvider();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
AuthenticationManager getAuthenticationManager(H http) {
|
||||
if (this.jwtConfigurer != null) {
|
||||
return this.jwtConfigurer.getAuthenticationManager(http);
|
||||
|
||||
+3
-2
@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@@ -87,11 +88,11 @@ class ServerHttpSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
|
||||
AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver) {
|
||||
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) {
|
||||
return new WebFluxConfigurer() {
|
||||
@Override
|
||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||
configurer.addCustomResolver(authenticationPrincipalArgumentResolver);
|
||||
configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+120
-22
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -55,6 +57,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
@@ -76,9 +79,11 @@ import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserSer
|
||||
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter;
|
||||
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
@@ -103,6 +108,7 @@ import org.springframework.security.web.server.DelegatingServerAuthenticationEnt
|
||||
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||
@@ -984,9 +990,9 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerWebExchangeMatcher authenticationMatcher;
|
||||
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
/**
|
||||
* Configures the {@link ReactiveAuthenticationManager} to use. The default is
|
||||
@@ -1028,6 +1034,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationFailureHandler} used after authentication failure.
|
||||
* Defaults to {@link RedirectServerAuthenticationFailureHandler} redirecting to "/login?error".
|
||||
*
|
||||
* @since 5.2
|
||||
* @param authenticationFailureHandler the failure handler to use
|
||||
@@ -1084,7 +1091,13 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationConverter getAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||
if (this.authenticationConverter == null) {
|
||||
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
|
||||
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;
|
||||
}
|
||||
@@ -1172,24 +1185,76 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
|
||||
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
|
||||
|
||||
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
|
||||
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
|
||||
authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
|
||||
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||
MediaType.TEXT_HTML);
|
||||
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
Map<String, String> urlToText = http.oauth2Login.getLinks();
|
||||
if (urlToText.size() == 1) {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
|
||||
} else {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
|
||||
}
|
||||
setDefaultEntryPoints(http);
|
||||
|
||||
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
|
||||
private void setDefaultEntryPoints(ServerHttpSecurity http) {
|
||||
String defaultLoginPage = "/login";
|
||||
Map<String, String> urlToText = http.oauth2Login.getLinks();
|
||||
String providerLoginPage = null;
|
||||
if (urlToText.size() == 1) {
|
||||
providerLoginPage = urlToText.keySet().iterator().next();
|
||||
}
|
||||
|
||||
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"),
|
||||
MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
|
||||
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
|
||||
ServerWebExchangeMatcher xhrMatcher = exchange -> {
|
||||
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
};
|
||||
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
|
||||
|
||||
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(
|
||||
notXhrMatcher, htmlMatcher);
|
||||
|
||||
if (providerLoginPage != null) {
|
||||
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage);
|
||||
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
|
||||
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
|
||||
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
|
||||
|
||||
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
|
||||
notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
|
||||
RedirectServerAuthenticationEntryPoint entryPoint =
|
||||
new RedirectServerAuthenticationEntryPoint(providerLoginPage);
|
||||
entryPoint.setRequestCache(http.requestCache.requestCache);
|
||||
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
|
||||
}
|
||||
|
||||
RedirectServerAuthenticationEntryPoint defaultEntryPoint =
|
||||
new RedirectServerAuthenticationEntryPoint(defaultLoginPage);
|
||||
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
|
||||
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandler == null) {
|
||||
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
|
||||
handler.setRequestCache(http.requestCache.requestCache);
|
||||
this.authenticationSuccessHandler = handler;
|
||||
}
|
||||
return this.authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
|
||||
if (this.authenticationFailureHandler == null) {
|
||||
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
|
||||
}
|
||||
return this.authenticationFailureHandler;
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
|
||||
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
|
||||
}
|
||||
@@ -1766,6 +1831,28 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
}
|
||||
|
||||
private class BearerTokenAuthenticationWebFilter extends AuthenticationWebFilter {
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
BearerTokenAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
|
||||
super(authenticationManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
return super.filter(exchange, chain)
|
||||
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
|
||||
.onAuthenticationFailure(webFilterExchange, e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticationFailureHandler(ServerAuthenticationFailureHandler authenticationFailureHandler) {
|
||||
super.setAuthenticationFailureHandler(authenticationFailureHandler);
|
||||
this.authenticationFailureHandler = authenticationFailureHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures JWT Resource Server Support
|
||||
*/
|
||||
@@ -1839,7 +1926,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
protected void configure(ServerHttpSecurity http) {
|
||||
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
|
||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
||||
AuthenticationWebFilter oauth2 = new BearerTokenAuthenticationWebFilter(authenticationManager);
|
||||
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
|
||||
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
|
||||
http
|
||||
@@ -1945,7 +2032,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
protected void configure(ServerHttpSecurity http) {
|
||||
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
|
||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
||||
AuthenticationWebFilter oauth2 = new BearerTokenAuthenticationWebFilter(authenticationManager);
|
||||
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
|
||||
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
|
||||
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
@@ -2568,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) {
|
||||
@@ -2765,7 +2852,9 @@ public class ServerHttpSecurity {
|
||||
protected void configure(ServerHttpSecurity http) {
|
||||
if (this.csrfTokenRepository != null) {
|
||||
this.filter.setCsrfTokenRepository(this.csrfTokenRepository);
|
||||
http.logout().addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
|
||||
if (ServerHttpSecurity.this.logout != null) {
|
||||
ServerHttpSecurity.this.logout.addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
|
||||
}
|
||||
}
|
||||
http.addFilterAt(this.filter, SecurityWebFiltersOrder.CSRF);
|
||||
}
|
||||
@@ -3022,8 +3111,12 @@ public class ServerHttpSecurity {
|
||||
public FormLoginSpec loginPage(String loginPage) {
|
||||
this.defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(loginPage);
|
||||
this.authenticationEntryPoint = this.defaultEntryPoint;
|
||||
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
|
||||
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
|
||||
if (this.requiresAuthenticationMatcher == null) {
|
||||
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
|
||||
}
|
||||
if (this.authenticationFailureHandler == null) {
|
||||
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -3640,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}
|
||||
@@ -3704,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) {
|
||||
|
||||
+13
-7
@@ -103,7 +103,9 @@ public class RSocketMessageHandlerITests {
|
||||
.data(data)
|
||||
.retrieveMono(String.class)
|
||||
.block()
|
||||
).isInstanceOf(ApplicationErrorException.class);
|
||||
).isInstanceOf(ApplicationErrorException.class)
|
||||
.hasMessageContaining("Access Denied");
|
||||
|
||||
assertThat(this.controller.payloads).isEmpty();
|
||||
}
|
||||
|
||||
@@ -116,7 +118,9 @@ public class RSocketMessageHandlerITests {
|
||||
.data(data)
|
||||
.retrieveMono(String.class)
|
||||
.block()
|
||||
).isInstanceOf(ApplicationErrorException.class);
|
||||
).isInstanceOf(ApplicationErrorException.class)
|
||||
.hasMessageContaining("Invalid Credentials");
|
||||
|
||||
assertThat(this.controller.payloads).isEmpty();
|
||||
}
|
||||
|
||||
@@ -149,12 +153,13 @@ public class RSocketMessageHandlerITests {
|
||||
@Test
|
||||
public void retrieveFluxWhenDataFluxAndSecureThenDenied() throws Exception {
|
||||
Flux<String> data = Flux.just("a", "b", "c");
|
||||
assertThatCode(() -> this.requester.route("secure.secure.retrieve-flux")
|
||||
assertThatCode(() -> this.requester.route("secure.retrieve-flux")
|
||||
.data(data, String.class)
|
||||
.retrieveFlux(String.class)
|
||||
.collectList()
|
||||
.block()).isInstanceOf(
|
||||
ApplicationErrorException.class);
|
||||
.block()
|
||||
).isInstanceOf(ApplicationErrorException.class)
|
||||
.hasMessageContaining("Access Denied");
|
||||
|
||||
assertThat(this.controller.payloads).isEmpty();
|
||||
}
|
||||
@@ -179,8 +184,9 @@ public class RSocketMessageHandlerITests {
|
||||
.data(data)
|
||||
.retrieveFlux(String.class)
|
||||
.collectList()
|
||||
.block()).isInstanceOf(
|
||||
ApplicationErrorException.class);
|
||||
.block()
|
||||
).isInstanceOf(ApplicationErrorException.class)
|
||||
.hasMessageContaining("Access Denied");
|
||||
|
||||
assertThat(this.controller.payloads).isEmpty();
|
||||
}
|
||||
|
||||
+49
-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.
|
||||
@@ -28,6 +28,7 @@ import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
@@ -36,6 +37,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Operators;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
@@ -139,6 +141,52 @@ public class SecurityReactorContextConfigurationTests {
|
||||
assertThat(resultContext).isSameAs(parentContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSubscriberIfNecessaryWhenNotServletRequestAttributesThenStillCreate() {
|
||||
RequestContextHolder.setRequestAttributes(
|
||||
new RequestAttributes() {
|
||||
@Override
|
||||
public Object getAttribute(String name, int scope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, int scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String name, int scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getAttributeNames(int scope) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerDestructionCallback(String name, Runnable callback, int scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveReference(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSessionMutex() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
CoreSubscriber<Object> subscriber = this.subscriberRegistrar.createSubscriberIfNecessary(Operators.emptySubscriber());
|
||||
assertThat(subscriber).isInstanceOf(SecurityReactorContextConfiguration.SecurityReactorContextSubscriber.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPublisherWhenLastOperatorAddedThenSecurityContextAttributesAvailable() {
|
||||
// Trigger the importing of SecurityReactorContextConfiguration via OAuth2ImportSelector
|
||||
|
||||
+29
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +32,8 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
|
||||
@@ -69,6 +71,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Joe Grandja
|
||||
* @author Evgeniy Cheban
|
||||
*/
|
||||
public class WebSecurityConfigurationTests {
|
||||
@Rule
|
||||
@@ -290,6 +293,31 @@ public class WebSecurityConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityExpressionHandlerWhenRoleHierarchyBeanThenRoleHierarchyUsed() {
|
||||
this.spring.register(WebSecurityExpressionHandlerRoleHierarchyBeanConfig.class).autowire();
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "notused", "ROLE_ADMIN");
|
||||
FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest("GET", ""),
|
||||
new MockHttpServletResponse(), new MockFilterChain());
|
||||
|
||||
AbstractSecurityExpressionHandler handler = this.spring.getContext().getBean(AbstractSecurityExpressionHandler.class);
|
||||
EvaluationContext evaluationContext = handler.createEvaluationContext(authentication, invocation);
|
||||
Expression expression = handler.getExpressionParser()
|
||||
.parseExpression("hasRole('ROLE_USER')");
|
||||
boolean granted = expression.getValue(evaluationContext, Boolean.class);
|
||||
assertThat(granted).isTrue();
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class WebSecurityExpressionHandlerRoleHierarchyBeanConfig extends WebSecurityConfigurerAdapter {
|
||||
@Bean
|
||||
RoleHierarchy roleHierarchy() {
|
||||
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
|
||||
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
|
||||
return roleHierarchy;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEvaluatorUsed() {
|
||||
this.spring.register(WebSecurityExpressionHandlerPermissionEvaluatorBeanConfig.class).autowire();
|
||||
|
||||
+21
@@ -458,4 +458,25 @@ public class LogoutConfigurerTests {
|
||||
@EnableWebSecurity
|
||||
static class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
|
||||
this.spring.register(LogoutDisabledConfig.class).autowire();
|
||||
|
||||
this.mvc.perform(post("/logout")
|
||||
.with(csrf()))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class LogoutDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout()
|
||||
.disable();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -183,6 +183,21 @@ public class RequestCacheConfigurerTests {
|
||||
|
||||
// This is desirable since XHR requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsTextEventStreamThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores text/event-stream
|
||||
|
||||
// This is desirable since event-stream requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
|
||||
|
||||
+39
@@ -22,6 +22,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
@@ -44,10 +45,14 @@ import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
|
||||
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -60,6 +65,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@@ -329,6 +335,39 @@ public class ServletApiConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutServletApiWhenCsrfDisabled() throws Exception {
|
||||
ConfigurableWebApplicationContext context = this.spring.register(CsrfDisabledConfig.class).getContext();
|
||||
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)
|
||||
.apply(springSecurity())
|
||||
.build();
|
||||
MvcResult mvcResult = mockMvc.perform(get("/"))
|
||||
.andReturn();
|
||||
assertThat(mvcResult.getRequest().getSession(false)).isNull();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.csrf().disable();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class LogoutController {
|
||||
@GetMapping("/")
|
||||
String logout(HttpServletRequest request) throws ServletException {
|
||||
request.getSession().setAttribute("foo", "bar");
|
||||
request.logout();
|
||||
return "logout";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Filter> T getFilter(Class<T> filterClass) {
|
||||
return (T) getFilters().stream()
|
||||
.filter(filterClass::isInstance)
|
||||
|
||||
+31
-2
@@ -360,6 +360,18 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
|
||||
}
|
||||
|
||||
// gh-8031
|
||||
@Test
|
||||
public void getWhenAnonymousDisabledThenAllows() throws Exception {
|
||||
this.spring.register(RestOperationsConfig.class, AnonymousDisabledConfig.class).autowire();
|
||||
mockRestOperations(jwks("Default"));
|
||||
String token = token("ValidNoScopes");
|
||||
|
||||
this.mvc.perform(get("/authenticated")
|
||||
.with(bearerToken(token)))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
|
||||
throws Exception {
|
||||
@@ -730,7 +742,8 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
|
||||
@Test
|
||||
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
|
||||
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire())
|
||||
assertThatCode(() -> this.spring
|
||||
.register(MultipleBearerTokenResolverBeansConfig.class, JwtDecoderConfig.class).autowire())
|
||||
.isInstanceOf(BeanCreationException.class)
|
||||
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
|
||||
}
|
||||
@@ -1127,7 +1140,7 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.with(bearerToken("token")))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
|
||||
containsString("Provided token [token] isn't active")));
|
||||
containsString("Provided token isn't active")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1469,6 +1482,22 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AnonymousDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.anonymous().disable()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
+21
@@ -47,6 +47,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
|
||||
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.result.view.AbstractView;
|
||||
@@ -434,4 +436,23 @@ public class EnableWebFluxSecurityTests {
|
||||
Child() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-8596
|
||||
public void resolveAuthenticationPrincipalArgumentResolverFirstDoesNotCauseBeanCurrentlyInCreationException() {
|
||||
this.spring.register(EnableWebFluxSecurityConfiguration.class,
|
||||
ReactiveAuthenticationTestConfiguration.class,
|
||||
DelegatingWebFluxConfiguration.class).autowire();
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class EnableWebFluxSecurityConfiguration {
|
||||
/**
|
||||
* It is necessary to Autowire AuthenticationPrincipalArgumentResolver because it triggers eager loading of
|
||||
* AuthenticationPrincipalArgumentResolver bean which causes BeanCurrentlyInCreationException
|
||||
*/
|
||||
@Autowired
|
||||
AuthenticationPrincipalArgumentResolver resolver;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -17,7 +17,6 @@
|
||||
package org.springframework.security.config.test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.mock.web.MockServletConfig;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.util.InMemoryXmlWebApplicationContext;
|
||||
@@ -113,8 +112,10 @@ public class SpringTestContext implements Closeable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigurableApplicationContext getContext() {
|
||||
public ConfigurableWebApplicationContext getContext() {
|
||||
if (!this.context.isRunning()) {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
this.context.refresh();
|
||||
}
|
||||
return this.context;
|
||||
|
||||
+58
@@ -33,9 +33,11 @@ import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverB
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.csrf.CsrfToken;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -213,6 +215,62 @@ public class FormLoginTests {
|
||||
homePage.assertAt();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formLoginWhenCustomAuthenticationFailureHandlerThenUsed() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
.pathMatchers("/login", "/failure").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/failure"))
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class)
|
||||
.assertAt();
|
||||
|
||||
loginPage.loginForm()
|
||||
.username("invalid")
|
||||
.password("invalid")
|
||||
.submit(HomePage.class);
|
||||
|
||||
assertThat(driver.getCurrentUrl()).endsWith("/failure");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formLoginWhenCustomRequiresAuthenticationMatcherThenUsed() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
.pathMatchers("/login", "/sign-in").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.requiresAuthenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/sign-in"))
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
driver.get("http://localhost/sign-in");
|
||||
|
||||
assertThat(driver.getCurrentUrl()).endsWith("/login?error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticationSuccess() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
|
||||
+79
@@ -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;
|
||||
@@ -164,4 +165,82 @@ public class LogoutSpecTests {
|
||||
.assertAt()
|
||||
.assertLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenDisabledThenPostToLogoutDoesNothing() {
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin().and()
|
||||
.logout().disable()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
|
||||
.assertAt();
|
||||
|
||||
FormLoginTests.HomePage homePage = loginPage.loginForm()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.submit(FormLoginTests.HomePage.class);
|
||||
|
||||
homePage.assertAt();
|
||||
|
||||
FormLoginTests.DefaultLogoutPage.to(driver)
|
||||
.assertAt()
|
||||
.logout();
|
||||
|
||||
homePage
|
||||
.assertAt();
|
||||
}
|
||||
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
+123
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
@@ -70,6 +71,7 @@ import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidationException;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
@@ -81,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;
|
||||
@@ -100,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;
|
||||
|
||||
/**
|
||||
@@ -184,6 +190,22 @@ public class OAuth2LoginTests {
|
||||
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
|
||||
}
|
||||
|
||||
// gh-8118
|
||||
@Test
|
||||
public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() {
|
||||
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire();
|
||||
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
|
||||
}
|
||||
|
||||
@EnableWebFlux
|
||||
static class WebFluxConfig { }
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
static class OAuth2LoginWithSingleClientRegistrations {
|
||||
@Bean
|
||||
@@ -518,6 +540,85 @@ public class OAuth2LoginTests {
|
||||
verify(securityContextRepository).save(any(), any());
|
||||
}
|
||||
|
||||
// gh-5562
|
||||
@Test
|
||||
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
|
||||
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
|
||||
OAuth2LoginWithCustomBeansConfig.class).autowire();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(this.springSecurity)
|
||||
.build();
|
||||
|
||||
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
|
||||
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
|
||||
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
||||
|
||||
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
|
||||
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
|
||||
OAuth2Error oauth2Error = new OAuth2Error("invalid_request", "Invalid request", null);
|
||||
when(tokenResponseClient.getTokenResponse(any())).thenThrow(new OAuth2AuthenticationException(oauth2Error));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/login/oauth2/code/google")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.valueEquals("Location", "/login?error");
|
||||
}
|
||||
|
||||
// gh-6484
|
||||
@Test
|
||||
public void oauth2LoginWhenIdTokenValidationFailsThenDefaultRedirectToLogin() {
|
||||
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
|
||||
OAuth2LoginWithCustomBeansConfig.class).autowire();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(this.springSecurity)
|
||||
.build();
|
||||
|
||||
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
|
||||
|
||||
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
|
||||
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
|
||||
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
||||
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
|
||||
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
|
||||
.tokenType(accessToken.getTokenType())
|
||||
.scopes(accessToken.getScopes())
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
|
||||
when(tokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = config.jwtDecoderFactory;
|
||||
OAuth2Error oauth2Error = new OAuth2Error("invalid_id_token", "Invalid ID Token", null);
|
||||
when(jwtDecoderFactory.createDecoder(any())).thenReturn(token ->
|
||||
Mono.error(new JwtValidationException("ID Token validation failed", Collections.singleton(oauth2Error))));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/login/oauth2/code/google")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.valueEquals("Location", "/login?error");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class OAuth2LoginWithCustomBeansConfig {
|
||||
|
||||
@@ -585,7 +686,6 @@ public class OAuth2LoginTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void logoutWhenUsingOidcLogoutHandlerThenRedirects() {
|
||||
this.spring.register(OAuth2LoginConfigWithOidcLogoutSuccessHandler.class).autowire();
|
||||
@@ -619,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)))
|
||||
@@ -639,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
|
||||
|
||||
+11
@@ -172,6 +172,17 @@ public class OAuth2ResourceServerSpecTests {
|
||||
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenEmptyBearerTokenThenReturnsInvalidToken() {
|
||||
this.spring.register(PublicKeyConfig.class).autowire();
|
||||
|
||||
this.client.get()
|
||||
.headers(headers -> headers.add("Authorization", "Bearer "))
|
||||
.exchange()
|
||||
.expectStatus().isUnauthorized()
|
||||
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenValidTokenAndPublicKeyInLambdaThenReturnsOk() {
|
||||
this.spring.register(PublicKeyInLambdaConfig.class, RootController.class).autowire();
|
||||
|
||||
+64
-6
@@ -20,10 +20,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.test.util.ReflectionTestUtils.getField;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -35,12 +37,20 @@ import org.apache.http.HttpHeaders;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.publisher.TestPublisher;
|
||||
|
||||
@@ -60,7 +70,6 @@ import org.springframework.security.web.server.context.WebSessionServerSecurityC
|
||||
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
|
||||
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
||||
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.reactive.server.EntityExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.FluxExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
@@ -196,7 +205,7 @@ public class ServerHttpSecurityTests {
|
||||
.isNotPresent();
|
||||
|
||||
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
|
||||
assertThat(logoutHandler)
|
||||
.get()
|
||||
@@ -209,17 +218,17 @@ public class ServerHttpSecurityTests {
|
||||
|
||||
assertThat(getWebFilter(securityWebFilterChain, CsrfWebFilter.class))
|
||||
.get()
|
||||
.extracting(csrfWebFilter -> ReflectionTestUtils.getField(csrfWebFilter, "csrfTokenRepository"))
|
||||
.extracting(csrfWebFilter -> getField(csrfWebFilter, "csrfTokenRepository"))
|
||||
.isEqualTo(this.csrfTokenRepository);
|
||||
|
||||
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
|
||||
|
||||
assertThat(logoutHandler)
|
||||
.get()
|
||||
.isExactlyInstanceOf(DelegatingServerLogoutHandler.class)
|
||||
.extracting(delegatingLogoutHandler ->
|
||||
((List<ServerLogoutHandler>) ReflectionTestUtils.getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
|
||||
((List<ServerLogoutHandler>) getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
|
||||
.map(ServerLogoutHandler::getClass)
|
||||
.collect(Collectors.toList()))
|
||||
.isEqualTo(Arrays.asList(SecurityContextServerLogoutHandler.class, CsrfServerLogoutHandler.class));
|
||||
@@ -475,9 +484,58 @@ public class ServerHttpSecurityTests {
|
||||
verify(customServerCsrfTokenRepository).loadToken(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
|
||||
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
|
||||
|
||||
SecurityWebFilterChain securityFilterChain = this.http
|
||||
.oauth2Login()
|
||||
.clientRegistrationRepository(clientRegistrationRepository)
|
||||
.and()
|
||||
.authorizeExchange().anyExchange().authenticated()
|
||||
.and()
|
||||
.requestCache(c -> c.requestCache(requestCache))
|
||||
.build();
|
||||
|
||||
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
|
||||
client.get().uri("/test").exchange();
|
||||
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
|
||||
verify(requestCache).saveRequest(captor.capture());
|
||||
assertThat(captor.getValue().getRequest().getURI().toString()).isEqualTo("/test");
|
||||
|
||||
|
||||
OAuth2LoginAuthenticationWebFilter authenticationWebFilter =
|
||||
getWebFilter(securityFilterChain, OAuth2LoginAuthenticationWebFilter.class).get();
|
||||
Object handler = getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
assertThat(getField(handler, "requestCache")).isSameAs(requestCache);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConfigureAuthorizationRequestRepositoryForOAuth2Login() {
|
||||
ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = mock(ServerAuthorizationRequestRepository.class);
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request().build();
|
||||
|
||||
when(authorizationRequestRepository.removeAuthorizationRequest(any())).thenReturn(Mono.just(authorizationRequest));
|
||||
|
||||
SecurityWebFilterChain securityFilterChain = this.http
|
||||
.oauth2Login()
|
||||
.clientRegistrationRepository(clientRegistrationRepository)
|
||||
.authorizationRequestRepository(authorizationRequestRepository)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
|
||||
client.get().uri("/login/oauth2/code/registration-id").exchange();
|
||||
|
||||
verify(authorizationRequestRepository).removeAuthorizationRequest(any());
|
||||
}
|
||||
|
||||
private boolean isX509Filter(WebFilter filter) {
|
||||
try {
|
||||
Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");
|
||||
Object converter = getField(filter, "authenticationConverter");
|
||||
return converter.getClass().isAssignableFrom(ServerX509AuthenticationConverter.class);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// field doesn't exist
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager implement
|
||||
|
||||
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
|
||||
|
||||
private Scheduler scheduler = Schedulers.newParallel("password-encoder", Schedulers.DEFAULT_POOL_SIZE, true);
|
||||
private Scheduler scheduler = Schedulers.boundedElastic();
|
||||
|
||||
private UserDetailsChecker preAuthenticationChecks = user -> {
|
||||
if (!user.isAccountNonLocked()) {
|
||||
|
||||
@@ -213,7 +213,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
((CredentialsContainer) result).eraseCredentials();
|
||||
}
|
||||
|
||||
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
|
||||
// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
|
||||
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
|
||||
if (parentResult == null) {
|
||||
eventPublisher.publishAuthenticationSuccess(result);
|
||||
@@ -230,7 +230,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
"No AuthenticationProvider found for {0}"));
|
||||
}
|
||||
|
||||
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
|
||||
// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
|
||||
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
|
||||
if (parentException == null) {
|
||||
prepareException(lastException, authentication);
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
|
||||
* </property>
|
||||
* </pre>
|
||||
*
|
||||
* A configuration note: The JaasAuthenticationProvider uses the security properites
|
||||
* A configuration note: The JaasAuthenticationProvider uses the security properties
|
||||
* "login.config.url.X" to configure jaas. If you would like to customize the way Jaas
|
||||
* gets configured, create a subclass of this and override the
|
||||
* {@link #configureJaas(Resource)} method.
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.SpringVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
@@ -108,8 +109,9 @@ public class SpringSecurityCoreVersion {
|
||||
*/
|
||||
private static String getSpringVersion() {
|
||||
Properties properties = new Properties();
|
||||
try {
|
||||
properties.load(SpringSecurityCoreVersion.class.getClassLoader().getResourceAsStream("META-INF/spring-security.versions"));
|
||||
try (InputStream is = SpringSecurityCoreVersion.class.getClassLoader()
|
||||
.getResourceAsStream("META-INF/spring-security.versions")) {
|
||||
properties.load(is);
|
||||
} catch (IOException | NullPointerException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found
|
||||
JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority
|
||||
JdbcDaoImpl.notFound=User {0} not found
|
||||
LdapAuthenticationProvider.badCredentials=Bad credentials
|
||||
LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
|
||||
LdapAuthenticationProvider.credentialsExpired=User credentials have expired
|
||||
LdapAuthenticationProvider.disabled=User is disabled
|
||||
LdapAuthenticationProvider.expired=User account has expired
|
||||
|
||||
+8
@@ -99,6 +99,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
}
|
||||
|
||||
public String encode(CharSequence rawPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
|
||||
String salt;
|
||||
if (random != null) {
|
||||
salt = BCrypt.gensalt(version.getVersion(), strength, random);
|
||||
@@ -109,6 +113,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
}
|
||||
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
|
||||
if (encodedPassword == null || encodedPassword.length() == 0) {
|
||||
logger.warn("Empty encoded password");
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2016 the original author or authors.
|
||||
* Copyright 2011-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,16 +32,13 @@ 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
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
|
||||
* (instead of GCM)
|
||||
*/
|
||||
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt,
|
||||
@@ -53,13 +50,21 @@ 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>
|
||||
* and does not provide any guarantees about the authenticity of the data.
|
||||
* For a more secure alternative, users should prefer
|
||||
* {@link #stronger(CharSequence, CharSequence)}.
|
||||
*
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #stronger(CharSequence, CharSequence), which uses the significatly more secure
|
||||
* GCM (instead of CBC)
|
||||
*/
|
||||
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt,
|
||||
@@ -100,7 +105,10 @@ public class Encryptors {
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* secret key
|
||||
* @deprecated This encryptor is not secure. Instead, look to your data store for a
|
||||
* mechanism to query encrypted data.
|
||||
*/
|
||||
@Deprecated
|
||||
public static TextEncryptor queryableText(CharSequence password, CharSequence salt) {
|
||||
return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
|
||||
salt));
|
||||
|
||||
-2
@@ -83,8 +83,6 @@ public class Md4PasswordEncoder implements PasswordEncoder {
|
||||
private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
|
||||
private boolean encodeHashAsBase64;
|
||||
|
||||
private Digester digester;
|
||||
|
||||
|
||||
public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
|
||||
this.encodeHashAsBase64 = encodeHashAsBase64;
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ package org.springframework.security.crypto.password;
|
||||
* @deprecated This PasswordEncoder is not secure. Instead use an
|
||||
* adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
|
||||
* SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
|
||||
* password upgrades.
|
||||
* password upgrades. There are no plans to remove this support. It is deprecated to indicate that
|
||||
* this is a legacy implementation and using it is considered insecure.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class NoOpPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
+12
@@ -200,4 +200,16 @@ public class BCryptPasswordEncoderTests {
|
||||
encoder.upgradeEncoding("not-a-bcrypt-password");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void encodeNullRawPassword() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
encoder.encode(null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void matchNullRawPassword() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
encoder.matches(null, "does-not-matter");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ asciidoctor {
|
||||
'gh-samples-url': "$ghUrl/samples"
|
||||
}
|
||||
|
||||
remotes {
|
||||
docs {
|
||||
retryCount = 5 // retry 5 times (default is 0)
|
||||
retryWaitSec = 10 // wait 10 seconds between retries (default is 0)
|
||||
}
|
||||
}
|
||||
|
||||
docsZip {
|
||||
from(project(':spring-security-docs-guides').asciidoctor) {
|
||||
into 'guides'
|
||||
|
||||
@@ -38,12 +38,12 @@ This configuration enables <<rsocket-authentication-basic,basic authentication>>
|
||||
|
||||
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
|
||||
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
|
||||
In a Spring Boot application this can be done using the following code.
|
||||
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ServerRSocketFactoryCustomizer springSecurityRSocketSecurity(
|
||||
ServerRSocketFactoryProcessor springSecurityRSocketSecurity(
|
||||
SecuritySocketAcceptorInterceptor interceptor) {
|
||||
return builder -> builder.addSocketAcceptorPlugin(interceptor);
|
||||
}
|
||||
|
||||
@@ -2372,8 +2372,8 @@ A bean identifier, used for referring to the bean elsewhere in the context.
|
||||
[[nsa-ldap-server-ldif]]
|
||||
* **ldif**
|
||||
Explicitly specifies an ldif file resource to load into an embedded LDAP server.
|
||||
The ldiff is should be a Spring resource pattern (i.e. classpath:init.ldiff).
|
||||
The default is classpath*:*.ldiff
|
||||
The ldif should be a Spring resource pattern (i.e. classpath:init.ldif).
|
||||
The default is classpath*:*.ldif
|
||||
|
||||
|
||||
[[nsa-ldap-server-manager-dn]]
|
||||
|
||||
@@ -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]
|
||||
----
|
||||
|
||||
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
|
||||
|
||||
[[spring-security-crypto-encryption-bytes]]
|
||||
=== BytesEncryptor
|
||||
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor:
|
||||
Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Encryptors.standard("password", "salt");
|
||||
Encryptors.stronger("password", "salt");
|
||||
----
|
||||
|
||||
The "standard" encryption method is 256-bit AES using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||
The "stronger" encryption method creates an encryptor using 256 bit AES encryption with
|
||||
Galois Counter Mode (GCM).
|
||||
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||
This method requires Java 6.
|
||||
The password used to generate the SecretKey should be kept in a secure place and not be shared.
|
||||
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
|
||||
@@ -38,6 +40,11 @@ Such a salt may be generated using a KeyGenerator:
|
||||
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
|
||||
----
|
||||
|
||||
Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
|
||||
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
|
||||
guarantees about the authenticity of the data.
|
||||
For a more secure alternative, users should prefer `Encryptors.stronger`.
|
||||
|
||||
[[spring-security-crypto-encryption-text]]
|
||||
=== TextEncryptor
|
||||
Use the Encryptors.text factory method to construct a standard TextEncryptor:
|
||||
|
||||
@@ -343,6 +343,36 @@ private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesM
|
||||
}
|
||||
----
|
||||
|
||||
The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`.
|
||||
When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead.
|
||||
|
||||
A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`.
|
||||
Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account.
|
||||
An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application.
|
||||
|
||||
The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public OAuth2AuthorizedClientManager authorizedClientManager(
|
||||
ClientRegistrationRepository clientRegistrationRepository,
|
||||
OAuth2AuthorizedClientService authorizedClientService) {
|
||||
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
OAuth2AuthorizedClientProviderBuilder.builder()
|
||||
.clientCredentials()
|
||||
.build();
|
||||
|
||||
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
|
||||
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientService);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[oauth2Client-auth-grant-support]]
|
||||
=== Authorization Grant Support
|
||||
|
||||
@@ -77,12 +77,12 @@ So long as this scheme is indicated, Resource Server will attempt to process the
|
||||
|
||||
Given a well-formed JWT, Resource Server will:
|
||||
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
|
||||
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
|
||||
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
[NOTE]
|
||||
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate JWTs.
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
|
||||
|
||||
@@ -456,9 +456,11 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
|
||||
JwtAuthenticationConverter jwtAuthenticationConverter =
|
||||
new JwtAuthenticationConverter();
|
||||
|
||||
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
|
||||
(new GrantedAuthoritiesExtractor());
|
||||
return jwtAuthenticationConveter;
|
||||
|
||||
return jwtAuthenticationConverter;
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
aspectjVersion=1.9.3
|
||||
gaeVersion=1.9.76
|
||||
springBootVersion=2.2.0.RELEASE
|
||||
version=5.2.2.BUILD-SNAPSHOT
|
||||
aspectjVersion=1.9.6
|
||||
gaeVersion=1.9.82
|
||||
springBootVersion=2.2.10.RELEASE
|
||||
version=5.2.7.RELEASE
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Dysprosium-SR1'
|
||||
ext.reactorVersion = 'Dysprosium-SR12'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.2.1.RELEASE'
|
||||
ext.springVersion = '5.2.9.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Moore-SR1'
|
||||
ext.springDataVersion = 'Moore-SR10'
|
||||
}
|
||||
|
||||
ext.rsocketVersion = '1.0.0-RC5'
|
||||
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.2'
|
||||
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.4'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.4'
|
||||
dependency 'org.powermock:powermock-core:2.0.4'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.4'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.4'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.4'
|
||||
dependency 'org.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.0'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.0'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.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,21 +58,21 @@ dependencyManagement {
|
||||
dependency 'com.nimbusds:lang-tag:1.4.3'
|
||||
dependency 'com.nimbusds:nimbus-jose-jwt:7.8.1'
|
||||
dependency 'com.nimbusds:oauth2-oidc-sdk:6.14'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.14.1'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.14.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.unboundid:unboundid-ldapsdk:4.0.12'
|
||||
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'
|
||||
dependency 'commons-codec:commons-codec:1.13'
|
||||
dependency 'commons-codec:commons-codec:1.14'
|
||||
dependency 'commons-collections:commons-collections:3.2.2'
|
||||
dependency 'commons-httpclient:commons-httpclient:3.1'
|
||||
dependency 'commons-io:commons-io:2.6'
|
||||
dependency 'commons-lang:commons-lang:2.6'
|
||||
dependency 'commons-logging:commons-logging:1.2'
|
||||
dependency 'dom4j:dom4j:1.6.1'
|
||||
dependency 'io.projectreactor.tools:blockhound:1.0.1.RELEASE'
|
||||
dependency 'io.projectreactor.tools:blockhound:1.0.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'
|
||||
@@ -138,19 +138,19 @@ dependencyManagement {
|
||||
dependency 'org.apache.directory.shared:shared-cursor:0.9.15'
|
||||
dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15'
|
||||
dependency 'org.apache.directory.shared:shared-ldap:0.9.15'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.10'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.12'
|
||||
dependency 'org.apache.httpcomponents:httpcore:4.4.8'
|
||||
dependency 'org.apache.httpcomponents:httpmime:4.5.3'
|
||||
dependency 'org.apache.mina:mina-core:2.0.0-M6'
|
||||
dependency 'org.apache.taglibs:taglibs-standard-impl:1.2.5'
|
||||
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
|
||||
dependency 'org.apache.taglibs:taglibs-standard-spec:1.2.5'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.24'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.24'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.24'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.24'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.24'
|
||||
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,21 +158,21 @@ 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.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
|
||||
dependency 'org.gebish:geb-ast:0.10.0'
|
||||
dependency 'org.gebish:geb-core:0.10.0'
|
||||
@@ -181,9 +181,9 @@ dependencyManagement {
|
||||
dependency 'org.hamcrest:hamcrest-core:1.3'
|
||||
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
|
||||
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.17.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.8.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.0.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.18.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.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'
|
||||
@@ -193,21 +193,21 @@ dependencyManagement {
|
||||
dependency 'org.mockito:mockito-core:3.0.0'
|
||||
dependency 'org.objenesis:objenesis:2.6'
|
||||
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
|
||||
dependency 'org.opensaml:opensaml-core:3.4.3'
|
||||
dependency 'org.opensaml:opensaml-saml-api:3.4.3'
|
||||
dependency 'org.opensaml:opensaml-saml-impl:3.4.3'
|
||||
dependency 'org.opensaml:opensaml-core:3.4.5'
|
||||
dependency 'org.opensaml:opensaml-saml-api:3.4.5'
|
||||
dependency 'org.opensaml:opensaml-saml-impl:3.4.5'
|
||||
dependency 'org.ow2.asm:asm:6.2.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.3'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.36.0'
|
||||
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
|
||||
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
|
||||
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
|
||||
dependency 'org.skyscreamer:jsonassert:1.5.0'
|
||||
dependency 'org.slf4j:jcl-over-slf4j:1.7.28'
|
||||
dependency 'org.slf4j:jul-to-slf4j:1.7.28'
|
||||
dependency 'org.slf4j:log4j-over-slf4j:1.7.28'
|
||||
dependency 'org.slf4j:slf4j-api:1.7.28'
|
||||
dependency 'org.slf4j:slf4j-nop:1.7.28'
|
||||
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
|
||||
dependency 'org.slf4j:jul-to-slf4j:1.7.30'
|
||||
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
|
||||
dependency 'org.slf4j:slf4j-api:1.7.30'
|
||||
dependency 'org.slf4j:slf4j-nop:1.7.30'
|
||||
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
|
||||
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
|
||||
dependency 'org.synchronoss.cloud:nio-multipart-parser:1.1.0'
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public class BasicAuthenticationTests extends AbstractWebServerIntegrationTests
|
||||
@Test
|
||||
public void httpBasicWhenAuthenticationRequiredAndNotAuthenticatedThen401() throws Exception {
|
||||
MockMvc mockMvc = createMockMvc("classpath:/spring/http-security-basic.xml", "classpath:/spring/in-memory-provider.xml", "classpath:/spring/testapp-servlet.xml");
|
||||
mockMvc.perform(get("secure/index"))
|
||||
mockMvc.perform(get("/secure/index"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public class ConcurrentSessionManagementTests extends AbstractWebServerIntegrati
|
||||
|
||||
MockMvc mockMvc = createMockMvc("classpath:/spring/http-security-concurrency.xml", "classpath:/spring/in-memory-provider.xml", "classpath:/spring/testapp-servlet.xml");
|
||||
|
||||
mockMvc.perform(get("secure/index").session(session1))
|
||||
mockMvc.perform(get("/secure/index").session(session1))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
|
||||
MockHttpServletRequestBuilder login1 = login()
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.ldap.server;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.ldap.core.ContextSource;
|
||||
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
|
||||
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
|
||||
|
||||
/**
|
||||
* Tests for {@link UnboundIdContainer}, specifically relating to LDIF file detection.
|
||||
*
|
||||
* @author Eleftheria Stein
|
||||
*/
|
||||
public class UnboundIdContainerLdifTests {
|
||||
|
||||
AnnotationConfigApplicationContext appCtx;
|
||||
|
||||
@After
|
||||
public void closeAppContext() {
|
||||
if (appCtx != null) {
|
||||
appCtx.close();
|
||||
appCtx = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unboundIdContainerWhenCustomLdifNameThenLdifLoaded() {
|
||||
appCtx = new AnnotationConfigApplicationContext(CustomLdifConfig.class);
|
||||
|
||||
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
|
||||
.getBean(ContextSource.class);
|
||||
|
||||
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
|
||||
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomLdifConfig {
|
||||
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||
"classpath:test-server.ldif");
|
||||
|
||||
@Bean
|
||||
UnboundIdContainer ldapContainer() {
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ContextSource contextSource(UnboundIdContainer container) {
|
||||
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
|
||||
+ container.getPort() + "/dc=springframework,dc=org");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unboundIdContainerWhenWildcardLdifNameThenLdifLoaded() {
|
||||
appCtx = new AnnotationConfigApplicationContext(WildcardLdifConfig.class);
|
||||
|
||||
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
|
||||
.getBean(ContextSource.class);
|
||||
|
||||
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
|
||||
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class WildcardLdifConfig {
|
||||
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||
"classpath*:test-server.ldif");
|
||||
|
||||
@Bean
|
||||
UnboundIdContainer ldapContainer() {
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ContextSource contextSource(UnboundIdContainer container) {
|
||||
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
|
||||
+ container.getPort() + "/dc=springframework,dc=org");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unboundIdContainerWhenMalformedLdifThenException() {
|
||||
try {
|
||||
appCtx = new AnnotationConfigApplicationContext(MalformedLdifConfig.class);
|
||||
failBecauseExceptionWasNotThrown(IllegalStateException.class);
|
||||
} catch (Exception e) {
|
||||
assertThat(e.getCause()).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(e.getMessage()).contains("Unable to load LDIF classpath:test-server-malformed.txt");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class MalformedLdifConfig {
|
||||
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
|
||||
"classpath:test-server-malformed.txt");
|
||||
|
||||
@Bean
|
||||
UnboundIdContainer ldapContainer() {
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
dn: ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn ou=subgroups,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: subgroups
|
||||
+17
-4
@@ -16,6 +16,7 @@
|
||||
package org.springframework.security.ldap.authentication.ad;
|
||||
|
||||
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
|
||||
|
||||
@@ -114,10 +114,10 @@ public class UnboundIdContainer implements InitializingBean, DisposableBean, Lif
|
||||
|
||||
private void importLdif(InMemoryDirectoryServer directoryServer) {
|
||||
if (StringUtils.hasText(this.ldif)) {
|
||||
Resource resource = this.context.getResource(this.ldif);
|
||||
try {
|
||||
if (resource.exists()) {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
Resource[] resources = this.context.getResources(this.ldif);
|
||||
if (resources.length > 0 && resources[0].exists()) {
|
||||
try (InputStream inputStream = resources[0].getInputStream()) {
|
||||
directoryServer.importFromLDIF(false, new LDIFReader(inputStream));
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -32,6 +32,7 @@ import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -58,6 +59,9 @@ import static org.springframework.security.ldap.authentication.ad.ActiveDirector
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
public static final String EXISTING_LDAP_PROVIDER = "ldap://192.168.1.200/";
|
||||
public static final String NON_EXISTING_LDAP_PROVIDER = "ldap://192.168.1.201/";
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@@ -378,16 +382,29 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test(expected = org.springframework.ldap.CommunicationException.class)
|
||||
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() {
|
||||
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
|
||||
msg));
|
||||
provider.authenticate(joe);
|
||||
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Throwable {
|
||||
try {
|
||||
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
|
||||
msg));
|
||||
provider.authenticate(joe);
|
||||
} catch (InternalAuthenticationServiceException e) {
|
||||
// Since GH-8418 ldap communication exception is wrapped into InternalAuthenticationServiceException.
|
||||
// This test is about the wrapped exception, so we throw it.
|
||||
throw e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = org.springframework.security.authentication.InternalAuthenticationServiceException.class )
|
||||
public void connectionExceptionIsWrappedInInternalException() throws Exception {
|
||||
ActiveDirectoryLdapAuthenticationProvider noneReachableProvider = new ActiveDirectoryLdapAuthenticationProvider(
|
||||
"mydomain.eu", NON_EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
|
||||
noneReachableProvider.doAuthentication(joe);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception {
|
||||
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
|
||||
"mydomain.eu", "ldap://192.168.1.200/", "dc=ad,dc=eu,dc=mydomain");
|
||||
"mydomain.eu", EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
|
||||
checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider);
|
||||
|
||||
}
|
||||
@@ -413,8 +430,11 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
provider.authenticate(joe);
|
||||
fail("CommunicationException was expected with a root cause of ClassNotFoundException");
|
||||
}
|
||||
catch (org.springframework.ldap.CommunicationException expected) {
|
||||
assertThat(expected.getRootCause()).isInstanceOf(ClassNotFoundException.class);
|
||||
catch (InternalAuthenticationServiceException expected) {
|
||||
assertThat(expected.getCause()).isInstanceOf(org.springframework.ldap.CommunicationException.class);
|
||||
org.springframework.ldap.CommunicationException cause =
|
||||
(org.springframework.ldap.CommunicationException) expected.getCause();
|
||||
assertThat(cause.getRootCause()).isInstanceOf(ClassNotFoundException.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.client;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
|
||||
* that is capable of operating outside of a {@code ServerHttpRequest} context,
|
||||
* e.g. in a scheduled/background thread and/or in the service-tier.
|
||||
*
|
||||
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
|
||||
*
|
||||
* @author Ankur Pathak
|
||||
* @author Phil Clay
|
||||
* @see ReactiveOAuth2AuthorizedClientManager
|
||||
* @see ReactiveOAuth2AuthorizedClientProvider
|
||||
* @see ReactiveOAuth2AuthorizedClientService
|
||||
* @since 5.2.2
|
||||
*/
|
||||
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
|
||||
implements ReactiveOAuth2AuthorizedClientManager {
|
||||
|
||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
|
||||
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
|
||||
|
||||
/**
|
||||
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
|
||||
*
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
* @param authorizedClientService the authorized client service
|
||||
*/
|
||||
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
this.authorizedClientService = authorizedClientService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
|
||||
|
||||
return createAuthorizationContext(authorizeRequest)
|
||||
.flatMap(this::authorizeAndSave);
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
|
||||
Authentication principal = authorizeRequest.getPrincipal();
|
||||
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
|
||||
.map(OAuth2AuthorizationContext::withAuthorizedClient)
|
||||
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
|
||||
.map(OAuth2AuthorizationContext::withAuthorizedClient)
|
||||
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
|
||||
.flatMap(contextBuilder -> this.contextAttributesMapper.apply(authorizeRequest)
|
||||
.defaultIfEmpty(Collections.emptyMap())
|
||||
.map(contextAttributes -> {
|
||||
OAuth2AuthorizationContext.Builder builder = contextBuilder.principal(principal);
|
||||
if (!contextAttributes.isEmpty()) {
|
||||
builder = builder.attributes(attributes -> attributes.putAll(contextAttributes));
|
||||
}
|
||||
return builder.build();
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
|
||||
return this.authorizedClientProvider.authorize(authorizationContext)
|
||||
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
|
||||
authorizedClient,
|
||||
authorizationContext.getPrincipal())
|
||||
.thenReturn(authorizedClient))
|
||||
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
|
||||
*
|
||||
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client
|
||||
*/
|
||||
public void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
|
||||
this.authorizedClientProvider = authorizedClientProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Function} used for mapping attribute(s) from the {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes
|
||||
* to be associated to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
|
||||
*
|
||||
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
|
||||
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
|
||||
*/
|
||||
public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
|
||||
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
|
||||
this.contextAttributesMapper = contextAttributesMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
|
||||
*/
|
||||
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
|
||||
|
||||
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper =
|
||||
new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
|
||||
|
||||
@Override
|
||||
public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -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(
|
||||
@@ -73,7 +88,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange(),
|
||||
accessTokenResponse.getAccessToken(),
|
||||
accessTokenResponse.getRefreshToken());
|
||||
accessTokenResponse.getRefreshToken(),
|
||||
accessTokenResponse.getAdditionalParameters());
|
||||
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
|
||||
+17
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,9 +22,13 @@ import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessT
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -55,8 +59,8 @@ import java.util.function.Function;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements
|
||||
ReactiveAuthenticationManager {
|
||||
public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
|
||||
public OAuth2AuthorizationCodeReactiveAuthenticationManager(
|
||||
@@ -70,7 +74,16 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements
|
||||
return Mono.defer(() -> {
|
||||
OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
|
||||
|
||||
OAuth2AuthorizationExchangeValidator.validate(token.getAuthorizationExchange());
|
||||
OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange().getAuthorizationResponse();
|
||||
if (authorizationResponse.statusError()) {
|
||||
return Mono.error(new OAuth2AuthorizationException(authorizationResponse.getError()));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange().getAuthorizationRequest();
|
||||
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
return Mono.error(new OAuth2AuthorizationException(oauth2Error));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
token.getClientRegistration(),
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.client.authentication;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
|
||||
/**
|
||||
* A validator for an "exchange" of an OAuth 2.0 Authorization Request and Response.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.1
|
||||
* @see OAuth2AuthorizationExchange
|
||||
*/
|
||||
final class OAuth2AuthorizationExchangeValidator {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_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);
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthorizationException(oauth2Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -60,7 +59,7 @@ import java.util.Map;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
|
||||
*/
|
||||
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
|
||||
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
|
||||
|
||||
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
|
||||
|
||||
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
|
||||
Assert.notNull(userService, "userService cannot be null");
|
||||
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
|
||||
OAuth2LoginAuthenticationToken loginAuthenticationToken =
|
||||
(OAuth2LoginAuthenticationToken) authentication;
|
||||
|
||||
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
// scope
|
||||
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
|
||||
if (authorizationCodeAuthentication.getAuthorizationExchange()
|
||||
if (loginAuthenticationToken.getAuthorizationExchange()
|
||||
.getAuthorizationRequest().getScopes().contains("openid")) {
|
||||
// This is an OpenID Connect Authentication Request so return null
|
||||
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
|
||||
return null;
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse;
|
||||
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
|
||||
try {
|
||||
OAuth2AuthorizationExchangeValidator.validate(
|
||||
authorizationCodeAuthentication.getAuthorizationExchange());
|
||||
|
||||
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
||||
new OAuth2AuthorizationCodeGrantRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange()));
|
||||
|
||||
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
|
||||
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
loginAuthenticationToken.getClientRegistration(),
|
||||
loginAuthenticationToken.getAuthorizationExchange()));
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
OAuth2Error oauth2Error = ex.getError();
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
|
||||
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
|
||||
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
|
||||
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
|
||||
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
|
||||
|
||||
Collection<? extends GrantedAuthority> mappedAuthorities =
|
||||
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
|
||||
|
||||
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange(),
|
||||
loginAuthenticationToken.getClientRegistration(),
|
||||
loginAuthenticationToken.getAuthorizationExchange(),
|
||||
oauth2User,
|
||||
mappedAuthorities,
|
||||
accessToken,
|
||||
accessTokenResponse.getRefreshToken());
|
||||
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
authorizationCodeAuthenticationToken.getRefreshToken());
|
||||
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
}
|
||||
|
||||
-6
@@ -78,7 +78,6 @@ import java.util.Map;
|
||||
*/
|
||||
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
||||
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
@@ -132,11 +131,6 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse;
|
||||
try {
|
||||
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
||||
|
||||
+7
-12
@@ -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.
|
||||
@@ -80,7 +80,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
|
||||
ReactiveAuthenticationManager {
|
||||
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
|
||||
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
|
||||
|
||||
@@ -122,18 +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());
|
||||
}
|
||||
|
||||
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
return Mono.error(new OAuth2AuthenticationException(
|
||||
oauth2Error, oauth2Error.toString()));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
@@ -145,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -172,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)
|
||||
|
||||
+6
-3
@@ -146,9 +146,12 @@ public final class ClientRegistrations {
|
||||
RequestEntity<Void> request = RequestEntity.get(uri).build();
|
||||
Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
|
||||
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
|
||||
return withProviderConfiguration(metadata, issuer.toASCIIString())
|
||||
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
||||
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
|
||||
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString())
|
||||
.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
|
||||
if (metadata.getUserInfoEndpointURI() != null) {
|
||||
builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
|
||||
}
|
||||
return builder;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@@ -121,9 +122,9 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
|
||||
private static HttpServletRequest getHttpServletRequestOrDefault(Map<String, Object> attributes) {
|
||||
HttpServletRequest servletRequest = (HttpServletRequest) attributes.get(HttpServletRequest.class.getName());
|
||||
if (servletRequest == null) {
|
||||
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (context != null) {
|
||||
servletRequest = context.getRequest();
|
||||
RequestAttributes context = RequestContextHolder.getRequestAttributes();
|
||||
if (context instanceof ServletRequestAttributes) {
|
||||
servletRequest = ((ServletRequestAttributes) context).getRequest();
|
||||
}
|
||||
}
|
||||
return servletRequest;
|
||||
@@ -132,9 +133,9 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
|
||||
private static HttpServletResponse getHttpServletResponseOrDefault(Map<String, Object> attributes) {
|
||||
HttpServletResponse servletResponse = (HttpServletResponse) attributes.get(HttpServletResponse.class.getName());
|
||||
if (servletResponse == null) {
|
||||
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (context != null) {
|
||||
servletResponse = context.getResponse();
|
||||
RequestAttributes context = RequestContextHolder.getRequestAttributes();
|
||||
if (context instanceof ServletRequestAttributes) {
|
||||
servletResponse = ((ServletRequestAttributes) context).getResponse();
|
||||
}
|
||||
}
|
||||
return servletResponse;
|
||||
|
||||
+3
-2
@@ -99,15 +99,16 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React
|
||||
private Mono<OAuth2AuthorizedClient> loadAuthorizedClient(String clientRegistrationId, Authentication principal, ServerWebExchange serverWebExchange) {
|
||||
return Mono.justOrEmpty(serverWebExchange)
|
||||
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
|
||||
.flatMap(exchange -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, exchange));
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizedClient> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, ServerWebExchange serverWebExchange) {
|
||||
return Mono.justOrEmpty(serverWebExchange)
|
||||
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
|
||||
.flatMap(exchange -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, exchange)
|
||||
.thenReturn(authorizedClient))
|
||||
.defaultIfEmpty(authorizedClient);
|
||||
.thenReturn(authorizedClient));
|
||||
}
|
||||
|
||||
private static Mono<ServerWebExchange> currentServerWebExchange() {
|
||||
|
||||
+33
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
@@ -132,24 +138,39 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (this.shouldProcessAuthorizationResponse(request)) {
|
||||
this.processAuthorizationResponse(request, response);
|
||||
if (matchesAuthorizationResponse(request)) {
|
||||
processAuthorizationResponse(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) {
|
||||
private boolean matchesAuthorizationResponse(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
|
||||
return false;
|
||||
}
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
|
||||
if (authorizationRequest == null) {
|
||||
return false;
|
||||
}
|
||||
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
|
||||
request.getServerPort(), request.getRequestURI(), null);
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
|
||||
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
|
||||
|
||||
// Compare redirect_uri
|
||||
UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
|
||||
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build();
|
||||
Set<Map.Entry<String, List<String>>> requestUriParameters = new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
|
||||
Set<Map.Entry<String, List<String>>> redirectUriParameters = new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
|
||||
// Remove the additional request parameters (if any) from the authorization response (request)
|
||||
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
|
||||
requestUriParameters.retainAll(redirectUriParameters);
|
||||
|
||||
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
|
||||
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
|
||||
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
|
||||
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
|
||||
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
|
||||
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
|
||||
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
String redirectUri = UrlUtils.buildFullRequestUrl(request);
|
||||
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
OAuth2Error error = ex.getError();
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationResponse.getRedirectUri())
|
||||
.fromUriString(authorizationRequest.getRedirectUri())
|
||||
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
|
||||
if (!StringUtils.isEmpty(error.getDescription())) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
|
||||
@@ -206,7 +224,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
|
||||
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
|
||||
|
||||
String redirectUrl = authorizationResponse.getRedirectUri();
|
||||
String redirectUrl = authorizationRequest.getRedirectUri();
|
||||
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||
if (savedRequest != null) {
|
||||
redirectUrl = savedRequest.getRedirectUrl();
|
||||
|
||||
+66
-1
@@ -22,6 +22,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
@@ -35,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
@@ -124,6 +126,17 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.clientCredentials()
|
||||
.password()
|
||||
.build();
|
||||
|
||||
// gh-7544
|
||||
if (authorizedClientRepository instanceof UnAuthenticatedServerOAuth2AuthorizedClientRepository) {
|
||||
UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager =
|
||||
new UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository,
|
||||
(UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository);
|
||||
unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
return unauthenticatedAuthorizedClientManager;
|
||||
}
|
||||
|
||||
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
@@ -266,7 +279,11 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.clientCredentials(this::updateClientCredentialsProvider)
|
||||
.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
||||
.build();
|
||||
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||
if (this.authorizedClientManager instanceof UnAuthenticatedReactiveOAuth2AuthorizedClientManager) {
|
||||
((UnAuthenticatedReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||
} else {
|
||||
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateClientCredentialsProvider(ReactiveOAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder builder) {
|
||||
@@ -376,4 +393,52 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class UnAuthenticatedReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
|
||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
private final UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
|
||||
private UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
||||
UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
|
||||
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
|
||||
|
||||
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
|
||||
Authentication principal = authorizeRequest.getPrincipal();
|
||||
|
||||
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
|
||||
.switchIfEmpty(Mono.defer(() -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, null)))
|
||||
.flatMap(authorizedClient -> {
|
||||
// Re-authorize
|
||||
return Mono.just(OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient).principal(principal).build())
|
||||
.flatMap(this.authorizedClientProvider::authorize)
|
||||
.flatMap(reauthorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(reauthorizedClient, principal, null).thenReturn(reauthorizedClient))
|
||||
// Default to the existing authorizedClient if the client was not re-authorized
|
||||
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
|
||||
authorizeRequest.getAuthorizedClient() : authorizedClient);
|
||||
})
|
||||
.switchIfEmpty(Mono.deferWithContext(context ->
|
||||
// Authorize
|
||||
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
||||
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
|
||||
.flatMap(clientRegistration -> Mono.just(OAuth2AuthorizationContext.withClientRegistration(clientRegistration).principal(principal).build()))
|
||||
.flatMap(this.authorizedClientProvider::authorize)
|
||||
.flatMap(authorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, null).thenReturn(authorizedClient))
|
||||
.subscriberContext(context)
|
||||
));
|
||||
}
|
||||
|
||||
private void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
|
||||
this.authorizedClientProvider = authorizedClientProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
@@ -389,15 +390,11 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
attrs.containsKey(HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
|
||||
return;
|
||||
}
|
||||
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
HttpServletRequest request = null;
|
||||
HttpServletResponse response = null;
|
||||
if (context != null) {
|
||||
request = context.getRequest();
|
||||
response = context.getResponse();
|
||||
RequestAttributes context = RequestContextHolder.getRequestAttributes();
|
||||
if (context instanceof ServletRequestAttributes) {
|
||||
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, ((ServletRequestAttributes) context).getRequest());
|
||||
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, ((ServletRequestAttributes) context).getResponse());
|
||||
}
|
||||
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request);
|
||||
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response);
|
||||
}
|
||||
|
||||
private void populateDefaultAuthentication(Map<String, Object> attrs) {
|
||||
|
||||
+5
-7
@@ -137,13 +137,11 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
|
||||
.switchIfEmpty(currentServerWebExchange());
|
||||
|
||||
return Mono.zip(defaultedRegistrationId, defaultedAuthentication, defaultedExchange)
|
||||
.map(t3 -> {
|
||||
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1()).principal(t3.getT2());
|
||||
if (t3.getT3() != null) {
|
||||
builder.attribute(ServerWebExchange.class.getName(), t3.getT3());
|
||||
}
|
||||
return builder.build();
|
||||
});
|
||||
.map(t3 -> OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1())
|
||||
.principal(t3.getT2())
|
||||
.attribute(ServerWebExchange.class.getName(), t3.getT3())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<Authentication> currentAuthentication() {
|
||||
|
||||
+50
-21
@@ -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.
|
||||
@@ -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;
|
||||
@@ -37,13 +39,20 @@ import org.springframework.security.web.server.authentication.ServerAuthenticati
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
* which handles the processing of the OAuth 2.0 Authorization Response.
|
||||
@@ -165,16 +174,22 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.requiresAuthenticationMatcher.matches(exchange)
|
||||
.filter( matchResult -> matchResult.isMatch())
|
||||
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.flatMap(matchResult ->
|
||||
this.authenticationConverter.convert(exchange)
|
||||
.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
|
||||
@@ -198,20 +213,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
}
|
||||
|
||||
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
|
||||
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||
.flatMap(authorizationRequest -> {
|
||||
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
|
||||
.query(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
|
||||
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
|
||||
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(queryParams)) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
})
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
return Mono.just(exchange)
|
||||
.filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
|
||||
.flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||
.flatMap(authorizationRequest ->
|
||||
matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
|
||||
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
|
||||
}
|
||||
|
||||
private static Mono<ServerWebExchangeMatcher.MatchResult> matchesRedirectUri(
|
||||
URI authorizationResponseUri, String authorizationRequestRedirectUri) {
|
||||
UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build();
|
||||
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build();
|
||||
Set<Map.Entry<String, List<String>>> requestUriParameters =
|
||||
new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
|
||||
Set<Map.Entry<String, List<String>>> redirectUriParameters =
|
||||
new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
|
||||
// Remove the additional request parameters (if any) from the authorization response (request)
|
||||
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
|
||||
requestUriParameters.retainAll(redirectUriParameters);
|
||||
|
||||
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
|
||||
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
|
||||
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
|
||||
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
|
||||
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
|
||||
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -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;
|
||||
@@ -28,13 +27,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -103,14 +101,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
|
||||
MultiValueMap<String, String> queryParams = exchange.getRequest()
|
||||
.getQueryParams();
|
||||
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
|
||||
.query(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
return OAuth2AuthorizationResponseUtils
|
||||
.convert(queryParams, redirectUri);
|
||||
.convert(exchange.getRequest().getQueryParams(), redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.client;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.test.publisher.PublisherProbe;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}.
|
||||
*
|
||||
* @author Ankur Pathak
|
||||
* @author Phil Clay
|
||||
*/
|
||||
public class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManagerTests {
|
||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper;
|
||||
private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
||||
private ClientRegistration clientRegistration;
|
||||
private Authentication principal;
|
||||
private OAuth2AuthorizedClient authorizedClient;
|
||||
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
|
||||
private PublisherProbe<Void> saveAuthorizedClientProbe;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Before
|
||||
public void setup() {
|
||||
this.clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
|
||||
this.authorizedClientService = mock(ReactiveOAuth2AuthorizedClientService.class);
|
||||
this.saveAuthorizedClientProbe = PublisherProbe.empty();
|
||||
when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(this.saveAuthorizedClientProbe.mono());
|
||||
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
|
||||
this.contextAttributesMapper = mock(Function.class);
|
||||
when(this.contextAttributesMapper.apply(any())).thenReturn(Mono.empty());
|
||||
this.authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, this.authorizedClientService);
|
||||
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
|
||||
this.authorizedClientManager.setContextAttributesMapper(this.contextAttributesMapper);
|
||||
this.clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
this.principal = new TestingAuthenticationToken("principal", "password");
|
||||
this.authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.scopes("read", "write"), TestOAuth2RefreshTokens.refreshToken());
|
||||
this.authorizationContextCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationContext.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(null, this.authorizedClientService))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("clientRegistrationRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(this.clientRegistrationRepository, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizedClientService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthorizedClientProviderWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.setAuthorizedClientProvider(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizedClientProvider cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setContextAttributesMapperWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.setContextAttributesMapper(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("contextAttributesMapper cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenRequestIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.authorize(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizeRequest cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
|
||||
String clientRegistrationId = "invalid-registration-id";
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)).thenReturn(Mono.empty());
|
||||
StepVerifier.create(this.authorizedClientManager.authorize(authorizeRequest))
|
||||
.verifyError(IllegalArgumentException.class);
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenNotAuthorizedAndUnsupportedProviderThenNotAuthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
when(this.authorizedClientService.loadAuthorizedClient(
|
||||
any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
when(authorizedClientProvider.authorize(any())).thenReturn(Mono.empty());
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient).verifyComplete();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isNull();
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService, never()).saveAuthorizedClient(
|
||||
any(OAuth2AuthorizedClient.class), eq(this.principal));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenNotAuthorizedAndSupportedProviderThenAuthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
|
||||
when(this.authorizedClientService.loadAuthorizedClient(
|
||||
any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(this.authorizedClient)
|
||||
.verifyComplete();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isNull();
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(this.authorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
when(this.authorizedClientService.loadAuthorizedClient(
|
||||
eq(this.clientRegistration.getRegistrationId()), eq(this.principal.getName()))).thenReturn(Mono.just(this.authorizedClient));
|
||||
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(reauthorizedClient)
|
||||
.verifyComplete();
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(reauthorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
|
||||
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(this.authorizedClient)
|
||||
.verifyComplete();
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService, never()).saveAuthorizedClient(
|
||||
any(OAuth2AuthorizedClient.class), eq(this.principal));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void reauthorizeWhenSupportedProviderThenReauthorized() {
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(reauthorizedClient)
|
||||
.verifyComplete();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(reauthorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void reauthorizeWhenRequestAttributeScopeThenMappedToContext() {
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||
|
||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
|
||||
.principal(this.principal)
|
||||
.attribute(OAuth2ParameterNames.SCOPE, "read write")
|
||||
.build();
|
||||
|
||||
this.authorizedClientManager.setContextAttributesMapper(new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
|
||||
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
|
||||
|
||||
StepVerifier.create(authorizedClient)
|
||||
.expectNext(reauthorizedClient)
|
||||
.verifyComplete();
|
||||
verify(this.authorizedClientService).saveAuthorizedClient(
|
||||
eq(reauthorizedClient), eq(this.principal));
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
assertThat(authorizationContext.getAttributes()).containsKey(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
|
||||
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
|
||||
assertThat(requestScopeAttribute).contains("read", "write");
|
||||
|
||||
}
|
||||
}
|
||||
+25
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
package org.springframework.security.oauth2.client.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -98,19 +100,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthorizationException() {
|
||||
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
|
||||
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(
|
||||
this.authorizationRequest, authorizationResponse);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
this.authenticationProvider.authenticate(
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
this.clientRegistration, authorizationExchange));
|
||||
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().refreshToken("refresh").build();
|
||||
@@ -132,4 +121,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
|
||||
assertThat(authenticationResult.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken());
|
||||
}
|
||||
|
||||
// gh-5368
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationSuccessResponseThenAdditionalParametersIncluded() {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("param1", "value1");
|
||||
additionalParameters.put("param2", "value2");
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
|
||||
.build();
|
||||
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
|
||||
|
||||
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest,
|
||||
success().build());
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication = (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider
|
||||
.authenticate(
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
|
||||
assertThat(authentication.getAdditionalParameters())
|
||||
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -81,13 +81,6 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests {
|
||||
.isInstanceOf(OAuth2AuthorizationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenRedirectUriNotEqualThenOAuth2AuthorizationException() {
|
||||
this.authorizationRequest.redirectUri("https://example.org/notequal");
|
||||
assertThatCode(() -> authenticate())
|
||||
.isInstanceOf(OAuth2AuthorizationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidThenSuccess() {
|
||||
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build()));
|
||||
|
||||
+1
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -151,20 +151,6 @@ public class OAuth2LoginAuthenticationProviderTests {
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||
this.exception.expect(OAuth2AuthenticationException.class);
|
||||
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
|
||||
|
||||
OAuth2AuthorizationResponse authorizationResponse =
|
||||
success().redirectUri("https://example2.com").build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
|
||||
|
||||
this.authenticationProvider.authenticate(
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
|
||||
|
||||
-13
@@ -186,19 +186,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||
this.exception.expect(OAuth2AuthenticationException.class);
|
||||
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
|
||||
|
||||
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
|
||||
|
||||
this.authenticationProvider.authenticate(
|
||||
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
|
||||
this.exception.expect(OAuth2AuthenticationException.class);
|
||||
|
||||
+8
@@ -195,6 +195,14 @@ public class ClientRegistrationsTest {
|
||||
assertThat(provider.getJwkSetUri()).isNull();
|
||||
}
|
||||
|
||||
// gh-8187
|
||||
@Test
|
||||
public void issuerWhenResponseMissingUserInfoUriThenSuccess() throws Exception {
|
||||
this.response.remove("userinfo_endpoint");
|
||||
ClientRegistration registration = registration("").build();
|
||||
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception {
|
||||
assertThat(registration("")).isNotNull();
|
||||
|
||||
+18
-29
@@ -65,6 +65,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
private MockServerWebExchange serverWebExchange;
|
||||
private Context context;
|
||||
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
|
||||
private PublisherProbe<OAuth2AuthorizedClient> loadAuthorizedClientProbe;
|
||||
private PublisherProbe<Void> saveAuthorizedClientProbe;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -74,8 +75,9 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
anyString())).thenReturn(Mono.empty());
|
||||
this.authorizedClientRepository = mock(ServerOAuth2AuthorizedClientRepository.class);
|
||||
this.loadAuthorizedClientProbe = PublisherProbe.empty();
|
||||
when(this.authorizedClientRepository.loadAuthorizedClient(
|
||||
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.loadAuthorizedClientProbe.mono());
|
||||
this.saveAuthorizedClientProbe = PublisherProbe.empty();
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(
|
||||
any(OAuth2AuthorizedClient.class), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.saveAuthorizedClientProbe.mono());
|
||||
@@ -131,6 +133,16 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
.hasMessage("authorizeRequest cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenExchangeIsNullThenThrowIllegalArgumentException() {
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
assertThatThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("serverWebExchange cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("invalid-registration-id")
|
||||
@@ -162,7 +174,8 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
assertThat(authorizedClient).isNull();
|
||||
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
|
||||
this.loadAuthorizedClientProbe.assertWasSubscribed();
|
||||
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -193,38 +206,14 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
this.saveAuthorizedClientProbe.assertWasSubscribed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenNotAuthorizedAndSupportedProviderAndExchangeUnavailableThenAuthorizedButNotSaved() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
|
||||
when(this.authorizedClientProvider.authorize(
|
||||
any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
|
||||
.principal(this.principal)
|
||||
.build();
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
|
||||
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
|
||||
|
||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
|
||||
assertThat(authorizationContext.getAuthorizedClient()).isNull();
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
assertThat(authorizedClient).isSameAs(this.authorizedClient);
|
||||
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||
this.loadAuthorizedClientProbe = PublisherProbe.of(Mono.just(this.authorizedClient));
|
||||
when(this.authorizedClientRepository.loadAuthorizedClient(
|
||||
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(Mono.just(this.authorizedClient));
|
||||
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(this.loadAuthorizedClientProbe.mono());
|
||||
|
||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||
this.clientRegistration, this.principal.getName(),
|
||||
@@ -313,7 +302,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
|
||||
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
|
||||
|
||||
assertThat(authorizedClient).isSameAs(this.authorizedClient);
|
||||
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
|
||||
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
+188
-119
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,17 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.client.web;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
@@ -50,13 +42,26 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2RefreshTokens.refreshToken;
|
||||
@@ -131,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
@@ -142,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
|
||||
String requestUri = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
request.setRequestURI(requestUri + "-no-match");
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestRedirectUriDoesNotMatchThenNotProcessed() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri);
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
authorizationResponse.setRequestURI(requestUri + "-no-match");
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
// gh-7963
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() throws Exception {
|
||||
// 1) redirect_uri with query parameters
|
||||
String requestUri = "/callback/client-1";
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
// 2) redirect_uri with query parameters AND authorization response additional parameters
|
||||
Map<String, String> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("auth-param1", "value1");
|
||||
additionalParameters.put("auth-param2", "value2");
|
||||
response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verifyZeroInteractions(filterChain);
|
||||
}
|
||||
|
||||
// gh-7963
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestRedirectUriParametersDoesNotMatchThenNotProcessed() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
// 1) Parameter value
|
||||
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.put("param2", "value8");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
// 2) Parameter order
|
||||
parametersNotMatch = new LinkedHashMap<>();
|
||||
parametersNotMatch.put("param2", "value2");
|
||||
parametersNotMatch.put("param1", "value1");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verify(filterChain, times(2)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
// 3) Parameter missing
|
||||
parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.remove("param2");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verify(filterChain, times(3)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestMatchThenAuthorizationRequestRemoved() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
|
||||
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(authorizationResponse)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
when(this.authenticationManager.authenticate(any(Authentication.class)))
|
||||
.thenThrow(new OAuth2AuthorizationException(error));
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), this.principalName1);
|
||||
@@ -241,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
|
||||
String requestUri = "/saved-request";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RequestCache requestCache = new HttpSessionRequestCache();
|
||||
requestCache.saveRequest(request, response);
|
||||
|
||||
requestUri = "/callback/client-1";
|
||||
request.setRequestURI(requestUri);
|
||||
request.setRequestURI("/callback/client-1");
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
@@ -284,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
AnonymousAuthenticationToken anonymousPrincipal =
|
||||
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(anonymousPrincipal);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), anonymousPrincipal, request);
|
||||
this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
|
||||
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
|
||||
assertThat(authorizedClient.getAccessToken()).isNotNull();
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
HttpSession session = authorizationResponse.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -325,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
SecurityContextHolder.setContext(securityContext); // null Authentication
|
||||
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), null, request);
|
||||
this.registration1.getRegistrationId(), null, authorizationResponse);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
|
||||
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
|
||||
assertThat(authorizedClient.getAccessToken()).isNotNull();
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
HttpSession session = authorizationResponse.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -362,13 +393,51 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient);
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationRequest(String requestUri) {
|
||||
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
if (!CollectionUtils.isEmpty(parameters)) {
|
||||
parameters.forEach(request::addParameter);
|
||||
request.setQueryString(
|
||||
parameters.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("&")));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationResponse(MockHttpServletRequest authorizationRequest) {
|
||||
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationResponse(
|
||||
MockHttpServletRequest authorizationRequest, Map<String, String> additionalParameters) {
|
||||
MockHttpServletRequest authorizationResponse = new MockHttpServletRequest(
|
||||
authorizationRequest.getMethod(), authorizationRequest.getRequestURI());
|
||||
authorizationResponse.setServletPath(authorizationRequest.getRequestURI());
|
||||
authorizationRequest.getParameterMap().forEach(authorizationResponse::addParameter);
|
||||
authorizationResponse.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
authorizationResponse.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
additionalParameters.forEach(authorizationResponse::addParameter);
|
||||
authorizationResponse.setQueryString(
|
||||
authorizationResponse.getParameterMap().entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue()[0])
|
||||
.collect(Collectors.joining("&")));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
return authorizationResponse;
|
||||
}
|
||||
|
||||
private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
|
||||
ClientRegistration registration) {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
|
||||
OAuth2AuthorizationRequest authorizationRequest = request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.redirectUri(request.getRequestURL().toString()).build();
|
||||
.attributes(attributes)
|
||||
.redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
|
||||
}
|
||||
|
||||
|
||||
+38
@@ -57,6 +57,7 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
@@ -587,6 +588,43 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
verify(this.authorizedClientRepository).loadAuthorizedClient(eq(this.registration.getRegistrationId()), any(), eq(this.serverWebExchange));
|
||||
}
|
||||
|
||||
// gh-7544
|
||||
@Test
|
||||
public void filterWhenClientCredentialsClientNotAuthorizedAndOutsideRequestContextThenGetNewToken() {
|
||||
// Use UnAuthenticatedServerOAuth2AuthorizedClientRepository when operating outside of a request context
|
||||
ServerOAuth2AuthorizedClientRepository unauthenticatedAuthorizedClientRepository = spy(new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
|
||||
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
|
||||
this.clientRegistrationRepository, unauthenticatedAuthorizedClientRepository);
|
||||
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
|
||||
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||
.expiresIn(360)
|
||||
.build();
|
||||
when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(Mono.just(registration));
|
||||
|
||||
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||
.attributes(clientRegistrationId(registration.getRegistrationId()))
|
||||
.build();
|
||||
|
||||
this.function.filter(request, this.exchange).block();
|
||||
|
||||
verify(unauthenticatedAuthorizedClientRepository).loadAuthorizedClient(any(), any(), any());
|
||||
verify(this.clientCredentialsTokenResponseClient).getTokenResponse(any());
|
||||
verify(unauthenticatedAuthorizedClientRepository).saveAuthorizedClient(any(), any(), any());
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(1);
|
||||
ClientRequest request1 = requests.get(0);
|
||||
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
|
||||
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(getBody(request1)).isEmpty();
|
||||
}
|
||||
|
||||
private Context serverWebExchange() {
|
||||
return Context.of(ServerWebExchange.class, this.serverWebExchange);
|
||||
}
|
||||
|
||||
+218
-38
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,25 +25,32 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.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.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.handler.DefaultWebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
@@ -102,7 +109,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
MockServerWebExchange exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/"));
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete());
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
|
||||
@@ -111,43 +118,216 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
|
||||
@Test
|
||||
public void filterWhenMatchThenAuthorizedClientSaved() {
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||
.redirectUri("/authorize/registration-id")
|
||||
.build();
|
||||
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
|
||||
.redirectUri("/authorize/registration-id")
|
||||
.build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||
ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
|
||||
Mono<Authentication> authentication = Mono.just(
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(registration, authorizationExchange));
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens
|
||||
.authenticated();
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||
.thenReturn(Mono.just(clientRegistration));
|
||||
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(
|
||||
Mono.just(authenticated));
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback");
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(authorizationRequest));
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
||||
.thenReturn(Mono.empty());
|
||||
ServerAuthenticationConverter converter = e -> authentication;
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
|
||||
|
||||
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
|
||||
this.authenticationManager, converter, this.authorizedClientRepository);
|
||||
this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/authorize/registration-id")
|
||||
.queryParam(OAuth2ParameterNames.CODE, "code")
|
||||
.queryParam(OAuth2ParameterNames.STATE, "state")
|
||||
.build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete());
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
|
||||
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any());
|
||||
}
|
||||
|
||||
// gh-7966
|
||||
@Test
|
||||
public void filterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() {
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||
.thenReturn(Mono.just(clientRegistration));
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
||||
.thenReturn(Mono.empty());
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
|
||||
|
||||
// 1) redirect_uri with query parameters
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback", parameters);
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verify(this.authenticationManager, times(1)).authenticate(any());
|
||||
|
||||
// 2) redirect_uri with query parameters AND authorization response additional parameters
|
||||
Map<String, String> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("auth-param1", "value1");
|
||||
additionalParameters.put("auth-param2", "value2");
|
||||
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
|
||||
exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verify(this.authenticationManager, times(2)).authenticate(any());
|
||||
}
|
||||
|
||||
// gh-7966
|
||||
@Test
|
||||
public void filterWhenAuthorizationRequestRedirectUriParametersNotMatchThenNotProcessed() {
|
||||
String requestUri = "/authorization/callback";
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest(requestUri, parameters);
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
|
||||
.thenReturn(Mono.just(oauth2AuthorizationRequest));
|
||||
|
||||
// 1) Parameter value
|
||||
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.put("param2", "value8");
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
|
||||
// 2) Parameter order
|
||||
parametersNotMatch = new LinkedHashMap<>();
|
||||
parametersNotMatch.put("param2", "value2");
|
||||
parametersNotMatch.put("param1", "value1");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
|
||||
// 3) Parameter missing
|
||||
parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.remove("param2");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
}
|
||||
|
||||
// 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<>();
|
||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
|
||||
return request()
|
||||
.attributes(attributes)
|
||||
.redirectUri(authorizationRequest.getURI().toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationRequest(String requestUri) {
|
||||
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
|
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
|
||||
.get(requestUri);
|
||||
if (!CollectionUtils.isEmpty(parameters)) {
|
||||
parameters.forEach(builder::queryParam);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationResponse(MockServerHttpRequest authorizationRequest) {
|
||||
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationResponse(
|
||||
MockServerHttpRequest authorizationRequest, Map<String, String> additionalParameters) {
|
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
|
||||
.get(authorizationRequest.getURI().toString());
|
||||
builder.queryParam(OAuth2ParameterNames.CODE, "code");
|
||||
builder.queryParam(OAuth2ParameterNames.STATE, "state");
|
||||
additionalParameters.forEach(builder::queryParam);
|
||||
builder.cookies(authorizationRequest.getCookies());
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,7 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -376,29 +377,34 @@ public final class OAuth2AuthorizationRequest implements Serializable {
|
||||
|
||||
private String buildAuthorizationRequestUri() {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, this.clientId);
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, encodeQueryParam(this.responseType.getValue()));
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, encodeQueryParam(this.clientId));
|
||||
if (!CollectionUtils.isEmpty(this.scopes)) {
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(this.scopes, " "));
|
||||
encodeQueryParam(StringUtils.collectionToDelimitedString(this.scopes, " ")));
|
||||
}
|
||||
if (this.state != null) {
|
||||
parameters.set(OAuth2ParameterNames.STATE, this.state);
|
||||
parameters.set(OAuth2ParameterNames.STATE, encodeQueryParam(this.state));
|
||||
}
|
||||
if (this.redirectUri != null) {
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri);
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, encodeQueryParam(this.redirectUri));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(this.additionalParameters)) {
|
||||
this.additionalParameters.forEach((k, v) -> parameters.set(k, v.toString()));
|
||||
this.additionalParameters.forEach((k, v) ->
|
||||
parameters.set(encodeQueryParam(k), encodeQueryParam(v.toString())));
|
||||
}
|
||||
|
||||
return UriComponentsBuilder.fromHttpUrl(this.authorizationUri)
|
||||
.queryParams(parameters)
|
||||
.encode(StandardCharsets.UTF_8)
|
||||
.build()
|
||||
.toUriString();
|
||||
}
|
||||
|
||||
// Encode query parameter value according to RFC 3986
|
||||
private static String encodeQueryParam(String value) {
|
||||
return UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> toLinkedHashSet(String... scope) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
Collections.addAll(result, scope);
|
||||
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,6 +43,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link HttpMessageConverter} for an {@link OAuth2AccessTokenResponse OAuth 2.0 Access Token Response}.
|
||||
@@ -55,8 +56,8 @@ import java.util.HashMap;
|
||||
public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@@ -80,10 +81,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
|
||||
throws HttpMessageNotReadableException {
|
||||
|
||||
try {
|
||||
// gh-6463
|
||||
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> tokenResponseParameters = (Map<String, String>) this.jsonMessageConverter.read(
|
||||
Map<String, Object> tokenResponseParameters = (Map<String, Object>) this.jsonMessageConverter.read(
|
||||
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
|
||||
return this.tokenResponseConverter.convert(tokenResponseParameters);
|
||||
return this.tokenResponseConverter.convert(
|
||||
tokenResponseParameters.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue().toString())));
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
|
||||
ex.getMessage(), ex, inputMessage);
|
||||
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,6 +34,7 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}.
|
||||
@@ -46,8 +47,8 @@ import java.util.Map;
|
||||
public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@@ -69,10 +70,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
|
||||
throws HttpMessageNotReadableException {
|
||||
|
||||
try {
|
||||
// gh-8157
|
||||
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> errorParameters = (Map<String, String>) this.jsonMessageConverter.read(
|
||||
Map<String, Object> errorParameters = (Map<String, Object>) this.jsonMessageConverter.read(
|
||||
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
|
||||
return this.errorConverter.convert(errorParameters);
|
||||
return this.errorConverter.convert(
|
||||
errorParameters.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> String.valueOf(entry.getValue()))));
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
|
||||
ex.getMessage(), ex, inputMessage);
|
||||
|
||||
+35
-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.
|
||||
@@ -307,4 +307,38 @@ public class OAuth2AuthorizationRequestTests {
|
||||
"response_type=code&client_id=client-id&state=state&" +
|
||||
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAuthorizationUriIncludesEscapedQueryParameterThenAuthorizationRequestUrlIncludesIt() {
|
||||
OAuth2AuthorizationRequest authorizationRequest =
|
||||
TestOAuth2AuthorizationRequests.request()
|
||||
.authorizationUri(AUTHORIZATION_URI +
|
||||
"?claims=%7B%22userinfo%22%3A%7B%22email_verified%22%3A%7B%22essential%22%3Atrue%7D%7D%7D").build();
|
||||
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://provider.com/oauth2/authorize?" +
|
||||
"claims=%7B%22userinfo%22%3A%7B%22email_verified%22%3A%7B%22essential%22%3Atrue%7D%7D%7D&" +
|
||||
"response_type=code&client_id=client-id&state=state&" +
|
||||
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenNonAsciiAdditionalParametersThenProperlyEncoded() {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("item amount", "19.95" + '\u20ac');
|
||||
additionalParameters.put("item name", "H" + '\u00c5' + "M" + '\u00d6');
|
||||
additionalParameters.put('\u00e2' + "ge", "4" + '\u00bd');
|
||||
OAuth2AuthorizationRequest authorizationRequest =
|
||||
TestOAuth2AuthorizationRequests.request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://example.com/login/oauth/authorize?" +
|
||||
"response_type=code&client_id=client-id&state=state&" +
|
||||
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id&" +
|
||||
"item%20amount=19.95%E2%82%AC&%C3%A2ge=4%C2%BD&item%20name=H%C3%85M%C3%96");
|
||||
}
|
||||
}
|
||||
|
||||
+34
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -96,6 +96,39 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-6463
|
||||
@Test
|
||||
public void readInternalWhenSuccessfulTokenResponseWithObjectThenReadOAuth2AccessTokenResponse() {
|
||||
String tokenResponse = "{\n" +
|
||||
" \"access_token\": \"access-token-1234\",\n" +
|
||||
" \"token_type\": \"bearer\",\n" +
|
||||
" \"expires_in\": 3600,\n" +
|
||||
" \"scope\": \"read write\",\n" +
|
||||
" \"refresh_token\": \"refresh-token-1234\",\n" +
|
||||
" \"custom_object_1\": {\"name1\": \"value1\"},\n" +
|
||||
" \"custom_object_2\": [\"value1\", \"value2\"],\n" +
|
||||
" \"custom_parameter_1\": \"custom-value-1\",\n" +
|
||||
" \"custom_parameter_2\": \"custom-value-2\"\n" +
|
||||
"}\n";
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(
|
||||
tokenResponse.getBytes(), HttpStatus.OK);
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = this.messageConverter.readInternal(
|
||||
OAuth2AccessTokenResponse.class, response);
|
||||
|
||||
assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBeforeOrEqualTo(Instant.now().plusSeconds(3600));
|
||||
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write");
|
||||
assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234");
|
||||
assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(
|
||||
entry("custom_object_1", "{name1=value1}"),
|
||||
entry("custom_object_2", "[value1, value2]"),
|
||||
entry("custom_parameter_1", "custom-value-1"),
|
||||
entry("custom_parameter_2", "custom-value-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
|
||||
Converter tokenResponseConverter = mock(Converter.class);
|
||||
|
||||
+20
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -78,6 +78,25 @@ public class OAuth2ErrorHttpMessageConverterTests {
|
||||
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
|
||||
}
|
||||
|
||||
// gh-8157
|
||||
@Test
|
||||
public void readInternalWhenErrorResponseWithObjectThenReadOAuth2Error() throws Exception {
|
||||
String errorResponse = "{\n" +
|
||||
" \"error\": \"unauthorized_client\",\n" +
|
||||
" \"error_description\": \"The client is not authorized\",\n" +
|
||||
" \"error_codes\": [65001],\n" +
|
||||
" \"error_uri\": \"https://tools.ietf.org/html/rfc6749#section-5.2\"\n" +
|
||||
"}\n";
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(
|
||||
errorResponse.getBytes(), HttpStatus.BAD_REQUEST);
|
||||
|
||||
OAuth2Error oauth2Error = this.messageConverter.readInternal(OAuth2Error.class, response);
|
||||
assertThat(oauth2Error.getErrorCode()).isEqualTo("unauthorized_client");
|
||||
assertThat(oauth2Error.getDescription()).isEqualTo("The client is not authorized");
|
||||
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
|
||||
Converter errorConverter = mock(Converter.class);
|
||||
|
||||
+6
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
@@ -28,6 +31,8 @@ import org.springframework.util.Assert;
|
||||
* @since 5.1
|
||||
*/
|
||||
public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static OAuth2Error INVALID_ISSUER =
|
||||
new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
@@ -57,6 +62,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
if (this.issuer.equals(tokenIssuer)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
} else {
|
||||
logger.debug(INVALID_ISSUER.getDescription());
|
||||
return OAuth2TokenValidatorResult.failure(INVALID_ISSUER);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-11
@@ -20,6 +20,9 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
@@ -41,6 +44,8 @@ import org.springframework.util.Assert;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
*/
|
||||
public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static final Duration DEFAULT_MAX_CLOCK_SKEW = Duration.of(60, ChronoUnit.SECONDS);
|
||||
|
||||
private final Duration clockSkew;
|
||||
@@ -56,7 +61,6 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
public JwtTimestampValidator(Duration clockSkew) {
|
||||
Assert.notNull(clockSkew, "clockSkew cannot be null");
|
||||
|
||||
this.clockSkew = clockSkew;
|
||||
}
|
||||
|
||||
@@ -71,11 +75,8 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
if (expiry != null) {
|
||||
if (Instant.now(this.clock).minus(clockSkew).isAfter(expiry)) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
String.format("Jwt expired at %s", jwt.getExpiresAt()),
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt()));
|
||||
return OAuth2TokenValidatorResult.failure(oAuth2Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,17 +84,22 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
if (notBefore != null) {
|
||||
if (Instant.now(this.clock).plus(clockSkew).isBefore(notBefore)) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
String.format("Jwt used before %s", jwt.getNotBefore()),
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore()));
|
||||
return OAuth2TokenValidatorResult.failure(oAuth2Error);
|
||||
}
|
||||
}
|
||||
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
|
||||
private OAuth2Error createOAuth2Error(String reason) {
|
||||
logger.debug(reason);
|
||||
return new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
reason,
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
}
|
||||
|
||||
/**
|
||||
* '
|
||||
* Use this {@link Clock} with {@link Instant#now()} for assessing
|
||||
|
||||
+13
-2
@@ -22,6 +22,7 @@ import java.net.URL;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -54,11 +55,13 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@@ -164,9 +167,17 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
private Jwt validateJwt(Jwt jwt){
|
||||
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
|
||||
if (result.hasErrors()) {
|
||||
String description = result.getErrors().iterator().next().getDescription();
|
||||
Collection<OAuth2Error> errors = result.getErrors();
|
||||
String validationErrorString = "Unable to validate Jwt";
|
||||
for (OAuth2Error oAuth2Error : errors) {
|
||||
if (!StringUtils.isEmpty(oAuth2Error.getDescription())) {
|
||||
validationErrorString = String.format(
|
||||
DECODING_ERROR_MESSAGE_TEMPLATE, oAuth2Error.getDescription());
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new JwtValidationException(
|
||||
String.format(DECODING_ERROR_MESSAGE_TEMPLATE, description),
|
||||
validationErrorString,
|
||||
result.getErrors());
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user