Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46fdb250dc | |||
| b500b3ea69 | |||
| 59171434d5 | |||
| 5d18dd6d7d | |||
| 41b0e51dbb | |||
| 310c1148ce | |||
| 93defb2ff2 | |||
| fb7efffad3 | |||
| 6db79b70e6 | |||
| 78a618c260 | |||
| cfc5256fad | |||
| 289b11b873 | |||
| 8dc702c80f | |||
| bd0247adef | |||
| 974156d5fb | |||
| e2121532a2 | |||
| 7cab7b06c5 | |||
| f60daa5152 | |||
| f63b770ec5 | |||
| 44bb975f82 | |||
| 3cb6b3e5d6 | |||
| db07cea579 | |||
| 0fb60c3aa7 | |||
| 31cb29cb2d | |||
| 46bfc00db2 | |||
| 987b14d1d4 | |||
| 1f19ee04e1 | |||
| 005eca7bd9 | |||
| 68ac3ef36b | |||
| 7a5c34ca57 | |||
| 542c625d7d | |||
| 4de2dbb4cd | |||
| 98399c920a | |||
| 52ad49074d | |||
| 6df5dc4ecf | |||
| 32acb04efe | |||
| 57dfbeecbb | |||
| 1181740f79 | |||
| 628ea00ad4 | |||
| 6dc22835fd | |||
| 174b71c017 | |||
| c7b4e4a493 | |||
| d03d31c2e7 | |||
| 9976eb9e3a | |||
| 7cf1ca7ab1 | |||
| 664d9f1ba6 | |||
| f7b160d7a8 | |||
| 5b60098548 | |||
| e6b383a9c4 | |||
| 5342133494 | |||
| 7498ad95a1 | |||
| 04d382d8d8 | |||
| 83a177d327 | |||
| ebb2afa5e8 | |||
| 276a934bcd | |||
| e4b538146a | |||
| 5948f492f5 | |||
| 1bac5498dd | |||
| 07479dce6d | |||
| 05c0176b98 | |||
| 03c2cc846d | |||
| ec7deca76f | |||
| 21c7f7518f | |||
| 0462c42290 | |||
| 21f7187f40 | |||
| 52814ab387 | |||
| 9c5ce287d7 | |||
| cb055f1402 | |||
| 8b3777d0f1 | |||
| 7181f576fc | |||
| dba3dacc19 | |||
| 5d0eb32f4d | |||
| 984fdd4c00 | |||
| 58b68625bb | |||
| 6a87ccca4a | |||
| 925a6685d9 | |||
| bd3e3ed2c5 | |||
| 6dad918e7b | |||
| 89ad42a06f | |||
| 85889d5e0b | |||
| 8c7dbf2e3d |
Vendored
+44
-22
@@ -8,6 +8,9 @@ properties(projectProperties)
|
||||
def SUCCESS = hudson.model.Result.SUCCESS.toString()
|
||||
currentBuild.result = SUCCESS
|
||||
|
||||
|
||||
def ARTIFACTORY_CREDENTIALS = usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')
|
||||
|
||||
try {
|
||||
parallel check: {
|
||||
stage('Check') {
|
||||
@@ -15,10 +18,13 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew clean check --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew clean check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
|
||||
currentBuild.result = 'FAILED: check'
|
||||
throw e
|
||||
} finally {
|
||||
@@ -34,11 +40,13 @@ try {
|
||||
sh "git clean -dfx"
|
||||
withCredentials([string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')]) {
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
if ("master" == env.BRANCH_NAME) {
|
||||
sh "./gradlew sonarqube -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
} else {
|
||||
sh "./gradlew sonarqube -PexcludeProjects='**/samples/**' -Dsonar.projectKey='spring-security-${env.BRANCH_NAME}' -Dsonar.projectName='spring-security-${env.BRANCH_NAME}' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
if ("master" == env.BRANCH_NAME) {
|
||||
sh "./gradlew sonarqube -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
} else {
|
||||
sh "./gradlew sonarqube -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD -PexcludeProjects='**/samples/**' -Dsonar.projectKey='spring-security-${env.BRANCH_NAME}' -Dsonar.projectName='spring-security-${env.BRANCH_NAME}' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
@@ -55,8 +63,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.2.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew clean test -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD -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'
|
||||
@@ -71,8 +81,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk9' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk9' }"]) {
|
||||
sh "./gradlew clean test -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk9'
|
||||
@@ -87,8 +99,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk10' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk10' }"]) {
|
||||
sh "./gradlew clean test -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk10'
|
||||
@@ -103,8 +117,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk11' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk11' }"]) {
|
||||
sh "./gradlew clean test -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk11'
|
||||
@@ -119,8 +135,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'openjdk12' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'openjdk12' }"]) {
|
||||
sh "./gradlew clean test -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk12'
|
||||
@@ -139,7 +157,7 @@ try {
|
||||
withCredentials([file(credentialsId: 'spring-signing-secring.gpg', variable: 'SIGNING_KEYRING_FILE')]) {
|
||||
withCredentials([string(credentialsId: 'spring-gpg-passphrase', variable: 'SIGNING_PASSWORD')]) {
|
||||
withCredentials([usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')]) {
|
||||
withCredentials([usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew deployArtifacts finalizeDeployArtifacts -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password='$SIGNING_PASSWORD' -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
@@ -156,8 +174,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew deployDocs -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew deployDocs -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,8 +189,10 @@ try {
|
||||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew deploySchema -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME --refresh-dependencies --no-daemon --stacktrace"
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS]) {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
|
||||
sh "./gradlew deploySchema -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-3
@@ -1,12 +1,20 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.23.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.23.1.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.6.RELEASE'
|
||||
classpath "io.freefair.gradle:aspectj-plugin:4.0.2"
|
||||
}
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/plugins-snapshot' }
|
||||
maven {
|
||||
url = 'https://repo.spring.io/plugins-snapshot'
|
||||
if (project.hasProperty('artifactoryUsername')) {
|
||||
credentials {
|
||||
username "$artifactoryUsername"
|
||||
password "$artifactoryPassword"
|
||||
}
|
||||
}
|
||||
}
|
||||
maven { url 'https://plugins.gradle.org/m2/' }
|
||||
}
|
||||
}
|
||||
@@ -35,3 +43,19 @@ subprojects {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty('artifactoryUsername')) {
|
||||
allprojects { project ->
|
||||
project.repositories { repos ->
|
||||
all { repo ->
|
||||
if (!repo.url.toString().startsWith("https://repo.spring.io/")) {
|
||||
return;
|
||||
}
|
||||
repo.credentials {
|
||||
username = artifactoryUsername
|
||||
password = artifactoryPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1933,9 +1933,9 @@ public final class HttpSecurity extends
|
||||
*
|
||||
* private RelyingPartyRegistration getSaml2RelyingPartyRegistration() {
|
||||
* //remote IDP entity ID
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||
* //remote WebSSO Endpoint - Where to Send AuthNRequests to
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
* //local registration ID
|
||||
* String registrationId = "simplesamlphp";
|
||||
* //local entity ID - autogenerated based on URL
|
||||
@@ -2024,9 +2024,9 @@ public final class HttpSecurity extends
|
||||
*
|
||||
* private RelyingPartyRegistration getSaml2RelyingPartyRegistration() {
|
||||
* //remote IDP entity ID
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||
* //remote WebSSO Endpoint - Where to Send AuthNRequests to
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
* //local registration ID
|
||||
* String registrationId = "simplesamlphp";
|
||||
* //local entity ID - autogenerated based on URL
|
||||
|
||||
+2
-2
@@ -183,7 +183,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
*
|
||||
* <p>
|
||||
* Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
|
||||
* "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* "https://web.archive.org/web/20160201174302/https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* >X-XSS-Protection header</a>
|
||||
* </p>
|
||||
*
|
||||
@@ -198,7 +198,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
*
|
||||
* <p>
|
||||
* Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
|
||||
* "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* "https://web.archive.org/web/20160201174302/https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
|
||||
* >X-XSS-Protection header</a>
|
||||
* </p>
|
||||
*
|
||||
|
||||
+3
-3
@@ -822,19 +822,19 @@ public class MiscHttpConfigTests {
|
||||
@RestController
|
||||
static class AuthenticationController {
|
||||
@GetMapping("/password")
|
||||
public String password(@AuthenticationPrincipal Authentication authentication) {
|
||||
public String password(Authentication authentication) {
|
||||
return (String) authentication.getCredentials();
|
||||
}
|
||||
|
||||
@GetMapping("/roles")
|
||||
public String roles(@AuthenticationPrincipal Authentication authentication) {
|
||||
public String roles(Authentication authentication) {
|
||||
return authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
@GetMapping("/details")
|
||||
public String details(@AuthenticationPrincipal Authentication authentication) {
|
||||
public String details(Authentication authentication) {
|
||||
return authentication.getDetails().getClass().getName();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -519,7 +519,7 @@ public class OAuth2ResourceServerSpecTests {
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
static class PlaceholderConfig {
|
||||
@Value("${classpath:org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub}")
|
||||
@Value("classpath:org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub")
|
||||
RSAPublicKey key;
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ You cannot have two separate sessions at once.
|
||||
So if you log in again in another window or tab you are just reauthenticating in the same session.
|
||||
The server doesn't know anything about tabs, windows or browser instances.
|
||||
All it sees are HTTP requests and it ties those to a particular session according to the value of the JSESSIONID cookie that they contain.
|
||||
When a user authenticates during a session, Spring Security's concurrent session control checks the number of__other authenticated sessions__ that they have.
|
||||
When a user authenticates during a session, Spring Security's concurrent session control checks the number of __other authenticated sessions__ that they have.
|
||||
If they are already authenticated with the same session, then re-authenticating will have no effect.
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
= Appendix
|
||||
|
||||
include::database-schema.adoc[]
|
||||
include::database-schema.adoc[leveloffset=+1]
|
||||
|
||||
include::namespace.adoc[]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -270,9 +270,9 @@ spring:
|
||||
saml2:
|
||||
login:
|
||||
relying-parties:
|
||||
- entity-id: &idp-entity-id https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php
|
||||
- entity-id: &idp-entity-id https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php
|
||||
registration-id: simplesamlphp
|
||||
web-sso-url: &idp-sso-url https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php
|
||||
web-sso-url: &idp-sso-url https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php
|
||||
signing-credentials: &service-provider-credentials
|
||||
- private-key: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
aspectjVersion=1.9.5
|
||||
gaeVersion=1.9.81
|
||||
springBootVersion=2.2.9.RELEASE
|
||||
version=5.2.6.RELEASE
|
||||
aspectjVersion=1.9.6
|
||||
gaeVersion=1.9.88
|
||||
springBootVersion=2.2.13.RELEASE
|
||||
version=5.2.10.RELEASE
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Dysprosium-SR10'
|
||||
ext.reactorVersion = 'Dysprosium-SR18'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.2.8.RELEASE'
|
||||
ext.springVersion = '5.2.13.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Moore-SR9'
|
||||
ext.springDataVersion = 'Moore-SR13'
|
||||
}
|
||||
|
||||
ext.rsocketVersion = '1.0.1'
|
||||
ext.rsocketVersion = '1.0.4'
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
@@ -24,12 +24,12 @@ dependencyManagement {
|
||||
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.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.powermock:powermock-api-mockito2:2.0.9'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.9'
|
||||
dependency 'org.powermock:powermock-core:2.0.9'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.9'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.9'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.9'
|
||||
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'
|
||||
@@ -44,7 +44,7 @@ dependencyManagement {
|
||||
dependency 'ch.qos.logback:logback-core:1.2.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.5'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.5'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.5'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.5.1'
|
||||
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'
|
||||
@@ -72,7 +72,7 @@ dependencyManagement {
|
||||
dependency 'commons-lang:commons-lang:2.6'
|
||||
dependency 'commons-logging:commons-logging:1.2'
|
||||
dependency 'dom4j:dom4j:1.6.1'
|
||||
dependency 'io.projectreactor.tools:blockhound:1.0.4.RELEASE'
|
||||
dependency 'io.projectreactor.tools:blockhound:1.0.6.RELEASE'
|
||||
dependency "io.rsocket:rsocket-core:${rsocketVersion}"
|
||||
dependency "io.rsocket:rsocket-transport-netty:${rsocketVersion}"
|
||||
dependency 'javax.activation:activation:1.1.1'
|
||||
@@ -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.12'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.13'
|
||||
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.37'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.37'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.37'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.37'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.45'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.45'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.45'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.45'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.57'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.37'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.45'
|
||||
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.20'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.20'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.20'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.21'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.21'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.21'
|
||||
dependency 'org.eclipse.jdt:ecj:3.12.3'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
|
||||
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.jetty.websocket:websocket-api:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.36.v20210114'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.36.v20210114'
|
||||
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'
|
||||
@@ -182,9 +182,9 @@ dependencyManagement {
|
||||
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
|
||||
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.18.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.13.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.2.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.5.0'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.4.30.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.1.7.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.5.1'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
|
||||
dependency 'org.javassist:javassist:3.22.0-CR2'
|
||||
dependency 'org.jboss.logging:jboss-logging:3.3.1.Final'
|
||||
@@ -193,9 +193,9 @@ 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.5'
|
||||
dependency 'org.opensaml:opensaml-saml-api:3.4.5'
|
||||
dependency 'org.opensaml:opensaml-saml-impl:3.4.5'
|
||||
dependency 'org.opensaml:opensaml-core:3.4.6'
|
||||
dependency 'org.opensaml:opensaml-saml-api:3.4.6'
|
||||
dependency 'org.opensaml:opensaml-saml-impl:3.4.6'
|
||||
dependency 'org.ow2.asm:asm:6.2.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.3'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.36.0'
|
||||
@@ -209,9 +209,9 @@ dependencyManagement {
|
||||
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.springframework.ldap:spring-ldap-core:2.3.3.RELEASE'
|
||||
dependency 'org.synchronoss.cloud:nio-multipart-parser:1.1.0'
|
||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.12.RELEASE'
|
||||
dependency 'org.unbescape:unbescape:1.1.5.RELEASE'
|
||||
dependency 'org.w3c.css:sac:1.3'
|
||||
dependency 'xalan:serializer:2.7.2'
|
||||
|
||||
+7
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.userinfo;
|
||||
|
||||
|
||||
@@ -22,6 +21,11 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
||||
import net.minidev.json.JSONObject;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -41,12 +45,6 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link ReactiveOAuth2UserService} that supports standard OAuth 2.0 Provider's.
|
||||
* <p>
|
||||
@@ -119,7 +117,7 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
||||
}
|
||||
Mono<Map<String, Object>> userAttributes = requestHeadersSpec
|
||||
.retrieve()
|
||||
.onStatus(s -> s != HttpStatus.OK, response -> parse(response).map(userInfoErrorResponse -> {
|
||||
.onStatus(HttpStatus::isError, response -> parse(response).map(userInfoErrorResponse -> {
|
||||
String description = userInfoErrorResponse.getErrorObject().getDescription();
|
||||
OAuth2Error oauth2Error = new OAuth2Error(
|
||||
INVALID_USER_INFO_RESPONSE_ERROR_CODE, description,
|
||||
|
||||
+20
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.userinfo;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -50,6 +49,7 @@ import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.mock;
|
||||
@@ -152,6 +152,24 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||
}
|
||||
|
||||
// gh-9336
|
||||
@Test
|
||||
public void loadUserWhenUserInfo201CreatedResponseThenReturnUser() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"id\": \"user1\",\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(new MockResponse().setResponseCode(201)
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(userInfoResponse));
|
||||
assertThatCode(() -> this.userService.loadUser(oauth2UserRequest()).block()).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
// gh-5500
|
||||
@Test
|
||||
public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception {
|
||||
|
||||
-6
@@ -56,12 +56,6 @@ final class ObjectToListStringConverter implements ConditionalGenericConverter {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (source instanceof List) {
|
||||
List<?> sourceList = (List<?>) source;
|
||||
if (!sourceList.isEmpty() && sourceList.get(0) instanceof String) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
if (source instanceof Collection) {
|
||||
Collection<String> results = new ArrayList<>();
|
||||
for (Object object : ((Collection<?>) source)) {
|
||||
|
||||
-3
@@ -52,9 +52,6 @@ final class ObjectToMapStringObjectConverter implements ConditionalGenericConver
|
||||
return null;
|
||||
}
|
||||
Map<?, ?> sourceMap = (Map<?, ?>) source;
|
||||
if (!sourceMap.isEmpty() && sourceMap.keySet().iterator().next() instanceof String) {
|
||||
return source;
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
sourceMap.forEach((k, v) -> result.put(k.toString(), v));
|
||||
return result;
|
||||
|
||||
+8
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -30,6 +30,8 @@ import java.util.SortedSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
||||
/**
|
||||
* The default implementation of an {@link OAuth2User}.
|
||||
*
|
||||
@@ -59,14 +61,16 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
||||
* @param attributes the attributes about the user
|
||||
* @param nameAttributeKey the key used to access the user's "name" from {@link #getAttributes()}
|
||||
*/
|
||||
public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) {
|
||||
Assert.notEmpty(authorities, "authorities cannot be empty");
|
||||
public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
|
||||
String nameAttributeKey) {
|
||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
|
||||
if (!attributes.containsKey(nameAttributeKey)) {
|
||||
throw new IllegalArgumentException("Missing attribute '" + nameAttributeKey + "' in attributes");
|
||||
}
|
||||
this.authorities = Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities)));
|
||||
this.authorities = (authorities != null)
|
||||
? Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities)))
|
||||
: Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES));
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
this.nameAttributeKey = nameAttributeKey;
|
||||
}
|
||||
|
||||
+38
-7
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import net.minidev.json.JSONArray;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
@@ -29,6 +30,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
@@ -141,9 +144,9 @@ public class ClaimConversionServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCollectionStringWhenListStringThenReturnSame() {
|
||||
public void convertCollectionStringWhenListStringThenReturnNotSameButEqual() {
|
||||
List<String> list = Lists.list("1", "2", "3", "4");
|
||||
assertThat(this.conversionService.convert(list, Collection.class)).isSameAs(list);
|
||||
assertThat(this.conversionService.convert(list, Collection.class)).isNotSameAs(list).isEqualTo(list);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -152,6 +155,17 @@ public class ClaimConversionServiceTests {
|
||||
.isEqualTo(Lists.list("1", "2", "3", "4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertListStringWhenJsonArrayThenConverts() {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
jsonArray.add("1");
|
||||
jsonArray.add("2");
|
||||
jsonArray.add("3");
|
||||
jsonArray.add(null);
|
||||
assertThat(this.conversionService.convert(jsonArray, List.class)).isNotInstanceOf(JSONArray.class)
|
||||
.isEqualTo(Lists.list("1", "2", "3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCollectionStringWhenNotConvertibleThenReturnSingletonList() {
|
||||
String string = "not-convertible-collection";
|
||||
@@ -165,9 +179,9 @@ public class ClaimConversionServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertListStringWhenListStringThenReturnSame() {
|
||||
public void convertListStringWhenListStringThenReturnNotSameButEqual() {
|
||||
List<String> list = Lists.list("1", "2", "3", "4");
|
||||
assertThat(this.conversionService.convert(list, List.class)).isSameAs(list);
|
||||
assertThat(this.conversionService.convert(list, List.class)).isNotSameAs(list).isEqualTo(list);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -189,7 +203,7 @@ public class ClaimConversionServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenMapStringObjectThenReturnSame() {
|
||||
public void convertMapStringObjectWhenMapStringObjectThenReturnNotSameButEqual() {
|
||||
Map<String, Object> mapStringObject = new HashMap<String, Object>() {
|
||||
{
|
||||
put("key1", "value1");
|
||||
@@ -197,7 +211,8 @@ public class ClaimConversionServiceTests {
|
||||
put("key3", "value3");
|
||||
}
|
||||
};
|
||||
assertThat(this.conversionService.convert(mapStringObject, Map.class)).isSameAs(mapStringObject);
|
||||
assertThat(this.conversionService.convert(mapStringObject, Map.class)).isNotSameAs(mapStringObject)
|
||||
.isEqualTo(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -219,6 +234,22 @@ public class ClaimConversionServiceTests {
|
||||
assertThat(this.conversionService.convert(mapIntegerObject, Map.class)).isEqualTo(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenJsonObjectThenConverts() {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("1", "value1");
|
||||
jsonObject.put("2", "value2");
|
||||
|
||||
Map<String, Object> mapStringObject = new HashMap<String, Object>() {
|
||||
{
|
||||
put("1", "value1");
|
||||
put("2", "value2");
|
||||
}
|
||||
};
|
||||
assertThat(this.conversionService.convert(jsonObject, Map.class)).isNotInstanceOf(JSONObject.class)
|
||||
.isEqualTo(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenNotConvertibleThenReturnNull() {
|
||||
List<String> notConvertibleList = Lists.list("1", "2", "3", "4");
|
||||
|
||||
+18
-5
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import net.minidev.json.JSONArray;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
@@ -45,6 +48,8 @@ public class ClaimTypeConverterTests {
|
||||
private static final String COLLECTION_STRING_CLAIM = "collection-string-claim";
|
||||
private static final String LIST_STRING_CLAIM = "list-string-claim";
|
||||
private static final String MAP_STRING_OBJECT_CLAIM = "map-string-object-claim";
|
||||
private static final String JSON_ARRAY_CLAIM = "json-array-claim";
|
||||
private static final String JSON_OBJECT_CLAIM = "json-object-claim";
|
||||
private ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
@Before
|
||||
@@ -107,7 +112,12 @@ public class ClaimTypeConverterTests {
|
||||
mapIntegerObject.put(1, "value1");
|
||||
Map<String, Object> mapStringObject = new HashMap<>();
|
||||
mapStringObject.put("1", "value1");
|
||||
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
jsonArray.add("1");
|
||||
List<String> jsonArrayListString = Lists.list("1");
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("1", "value1");
|
||||
Map<String, Object> jsonObjectMap = Maps.newHashMap("1", "value1");
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(STRING_CLAIM, Boolean.TRUE);
|
||||
claims.put(BOOLEAN_CLAIM, "true");
|
||||
@@ -116,7 +126,8 @@ public class ClaimTypeConverterTests {
|
||||
claims.put(COLLECTION_STRING_CLAIM, listNumber);
|
||||
claims.put(LIST_STRING_CLAIM, listNumber);
|
||||
claims.put(MAP_STRING_OBJECT_CLAIM, mapIntegerObject);
|
||||
|
||||
claims.put(JSON_ARRAY_CLAIM, jsonArray);
|
||||
claims.put(JSON_OBJECT_CLAIM, jsonObject);
|
||||
claims = this.claimTypeConverter.convert(claims);
|
||||
|
||||
assertThat(claims.get(STRING_CLAIM)).isEqualTo("true");
|
||||
@@ -126,6 +137,8 @@ public class ClaimTypeConverterTests {
|
||||
assertThat(claims.get(COLLECTION_STRING_CLAIM)).isEqualTo(listString);
|
||||
assertThat(claims.get(LIST_STRING_CLAIM)).isEqualTo(listString);
|
||||
assertThat(claims.get(MAP_STRING_OBJECT_CLAIM)).isEqualTo(mapStringObject);
|
||||
assertThat(claims.get(JSON_ARRAY_CLAIM)).isEqualTo(jsonArrayListString);
|
||||
assertThat(claims.get(JSON_OBJECT_CLAIM)).isEqualTo(jsonObjectMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,9 +166,9 @@ public class ClaimTypeConverterTests {
|
||||
assertThat(claims.get(BOOLEAN_CLAIM)).isSameAs(bool);
|
||||
assertThat(claims.get(INSTANT_CLAIM)).isSameAs(instant);
|
||||
assertThat(claims.get(URL_CLAIM)).isSameAs(url);
|
||||
assertThat(claims.get(COLLECTION_STRING_CLAIM)).isSameAs(listString);
|
||||
assertThat(claims.get(LIST_STRING_CLAIM)).isSameAs(listString);
|
||||
assertThat(claims.get(MAP_STRING_OBJECT_CLAIM)).isSameAs(mapStringObject);
|
||||
assertThat(claims.get(COLLECTION_STRING_CLAIM)).isNotSameAs(listString).isEqualTo(listString);
|
||||
assertThat(claims.get(LIST_STRING_CLAIM)).isNotSameAs(listString).isEqualTo(listString);
|
||||
assertThat(claims.get(MAP_STRING_OBJECT_CLAIM)).isNotSameAs(mapStringObject).isEqualTo(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+22
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2021 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,6 +18,7 @@ package org.springframework.security.oauth2.core.oidc.user;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
@@ -57,11 +58,6 @@ public class DefaultOidcUserTests {
|
||||
private static final OidcIdToken ID_TOKEN = new OidcIdToken("id-token-value", Instant.EPOCH, Instant.MAX, ID_TOKEN_CLAIMS);
|
||||
private static final OidcUserInfo USER_INFO = new OidcUserInfo(USER_INFO_CLAIMS);
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenAuthoritiesIsNullThenThrowIllegalArgumentException() {
|
||||
new DefaultOidcUser(null, ID_TOKEN);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenIdTokenIsNullThenThrowIllegalArgumentException() {
|
||||
new DefaultOidcUser(AUTHORITIES, null);
|
||||
@@ -72,6 +68,26 @@ public class DefaultOidcUserTests {
|
||||
new DefaultOidcUser(AUTHORITIES, ID_TOKEN, "invalid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesIsNullThenCreatedWithEmptyAuthorities() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(null, ID_TOKEN);
|
||||
assertThat(user.getClaims()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB);
|
||||
assertThat(user.getIdToken()).isEqualTo(ID_TOKEN);
|
||||
assertThat(user.getName()).isEqualTo(SUBJECT);
|
||||
assertThat(user.getAuthorities()).isEmpty();
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesIsEmptyThenCreated() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, ID_TOKEN);
|
||||
assertThat(user.getClaims()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB);
|
||||
assertThat(user.getIdToken()).isEqualTo(ID_TOKEN);
|
||||
assertThat(user.getName()).isEqualTo(SUBJECT);
|
||||
assertThat(user.getAuthorities()).isEmpty();
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesIdTokenProvidedThenCreated() {
|
||||
DefaultOidcUser user = new DefaultOidcUser(AUTHORITIES, ID_TOKEN);
|
||||
|
||||
+17
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2021 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,16 +41,6 @@ public class DefaultOAuth2UserTests {
|
||||
private static final Map<String, Object> ATTRIBUTES = Collections.singletonMap(
|
||||
ATTRIBUTE_NAME_KEY, USERNAME);
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenAuthoritiesIsNullThenThrowIllegalArgumentException() {
|
||||
new DefaultOAuth2User(null, ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenAuthoritiesIsEmptyThenThrowIllegalArgumentException() {
|
||||
new DefaultOAuth2User(Collections.emptySet(), ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenAttributesIsNullThenThrowIllegalArgumentException() {
|
||||
new DefaultOAuth2User(AUTHORITIES, null, ATTRIBUTE_NAME_KEY);
|
||||
@@ -71,6 +61,22 @@ public class DefaultOAuth2UserTests {
|
||||
new DefaultOAuth2User(AUTHORITIES, ATTRIBUTES, "invalid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesIsNullThenCreatedWithEmptyAuthorities() {
|
||||
DefaultOAuth2User user = new DefaultOAuth2User(null, ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||
assertThat(user.getName()).isEqualTo(USERNAME);
|
||||
assertThat(user.getAuthorities()).isEmpty();
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthoritiesIsEmptyThenCreated() {
|
||||
DefaultOAuth2User user = new DefaultOAuth2User(Collections.emptySet(), ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||
assertThat(user.getName()).isEqualTo(USERNAME);
|
||||
assertThat(user.getAuthorities()).isEmpty();
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(ATTRIBUTE_NAME_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAllParametersProvidedAndValidThenCreated() {
|
||||
DefaultOAuth2User user = new DefaultOAuth2User(AUTHORITIES, ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||
|
||||
+2
-2
@@ -37,9 +37,9 @@ import static org.springframework.util.Assert.notNull;
|
||||
* A fully configured registration may look like
|
||||
* <pre>
|
||||
* //remote IDP entity ID
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||
* //remote WebSSO Endpoint - Where to Send AuthNRequests to
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
* //local registration ID
|
||||
* String registrationId = "simplesamlphp";
|
||||
* //local entity ID - autogenerated based on URL
|
||||
|
||||
+6
-6
@@ -124,7 +124,7 @@ public class Saml2LoginIntegrationTests {
|
||||
public void authenticateRequestWhenUnauthenticatedThenRespondsWithRedirectAuthNRequestXML() throws Exception {
|
||||
mockMvc.perform(get("http://localhost:8080/saml2/authenticate/simplesamlphp"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest=")));
|
||||
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php?SAMLRequest=")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +134,7 @@ public class Saml2LoginIntegrationTests {
|
||||
.param("RelayState", "relay state value with spaces")
|
||||
)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest=")))
|
||||
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php?SAMLRequest=")))
|
||||
.andExpect(header().string("Location", containsString("RelayState=relay%20state%20value%20with%20spaces")));
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class Saml2LoginIntegrationTests {
|
||||
String destination = authnRequest.getDestination();
|
||||
assertEquals(
|
||||
"Destination must match",
|
||||
"https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php",
|
||||
"https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php",
|
||||
destination
|
||||
);
|
||||
String acsURL = authnRequest.getAssertionConsumerServiceURL();
|
||||
@@ -317,14 +317,14 @@ public class Saml2LoginIntegrationTests {
|
||||
Response response = OpenSamlActionTestingSupport.buildResponse();
|
||||
response.setID("_" + UUID.randomUUID().toString());
|
||||
response.setDestination("http://localhost:8080/login/saml2/sso/simplesamlphp");
|
||||
response.setIssuer(buildIssuer("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"));
|
||||
response.setIssuer(buildIssuer("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"));
|
||||
return response;
|
||||
}
|
||||
|
||||
private Assertion buildAssertion(String username) {
|
||||
Assertion assertion = OpenSamlActionTestingSupport.buildAssertion();
|
||||
assertion.setIssueInstant(DateTime.now());
|
||||
assertion.setIssuer(buildIssuer("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"));
|
||||
assertion.setIssuer(buildIssuer("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"));
|
||||
assertion.setSubject(buildSubject(username));
|
||||
assertion.setConditions(buildConditions());
|
||||
|
||||
@@ -346,7 +346,7 @@ public class Saml2LoginIntegrationTests {
|
||||
final PrivateKey privateKey = KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]);
|
||||
BasicCredential cred = CredentialSupport.getSimpleCredential(publicKey, privateKey);
|
||||
cred.setUsageType(usageType);
|
||||
cred.setEntityId("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php");
|
||||
cred.setEntityId("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php");
|
||||
return cred;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ spring:
|
||||
verification:
|
||||
credentials:
|
||||
- certificate-location: "classpath:credentials/idp-certificate.crt"
|
||||
entity-id: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php
|
||||
sso-url: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php
|
||||
entity-id: https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php
|
||||
sso-url: https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<a th:href="@{/logout}">Log Out</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://simplesaml-for-spring-saml.cfapps.io/module.php/core/authenticate.php?as=example-userpass&logout">
|
||||
<a href="https://simplesaml-for-spring-saml.apps.pcfone.io/module.php/core/authenticate.php?as=example-userpass&logout">
|
||||
Log out of SimpleSAMLPhp
|
||||
</a>
|
||||
</li>
|
||||
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile "org.bouncycastle:bcprov-jdk15on"
|
||||
compile "org.bouncycastle:bcpkix-jdk15on"
|
||||
compile slf4jDependencies
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
}
|
||||
|
||||
+4
@@ -27,6 +27,10 @@ import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||
public class MessageSecurityWebApplicationInitializer extends
|
||||
AbstractSecurityWebApplicationInitializer {
|
||||
|
||||
public MessageSecurityWebApplicationInitializer() {
|
||||
super(SecurityConfig.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enableHttpSessionEventPublisher() {
|
||||
return true;
|
||||
|
||||
+10
-13
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.security.samples.config;
|
||||
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -23,6 +25,7 @@ import org.springframework.security.converter.RsaKeyConverters;
|
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -39,11 +42,12 @@ import static org.springframework.security.saml2.credentials.Saml2X509Credential
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
RelyingPartyRegistration getSaml2AuthenticationConfiguration() throws Exception {
|
||||
@Bean
|
||||
RelyingPartyRegistrationRepository getSaml2AuthenticationConfiguration() throws Exception {
|
||||
//remote IDP entity ID
|
||||
String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
|
||||
String idpEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||
//remote WebSSO Endpoint - Where to Send AuthNRequests to
|
||||
String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
|
||||
String webSsoEndpoint = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
//local registration ID
|
||||
String registrationId = "simplesamlphp";
|
||||
//local entity ID - autogenerated based on URL
|
||||
@@ -53,14 +57,14 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
//IDP certificate for verification of incoming messages
|
||||
Saml2X509Credential idpVerificationCertificate = getVerificationCertificate();
|
||||
String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId)
|
||||
return new InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistration.withRegistrationId(registrationId)
|
||||
.remoteIdpEntityId(idpEntityId)
|
||||
.idpWebSsoUrl(webSsoEndpoint)
|
||||
.credentials(c -> c.add(signingCredential))
|
||||
.credentials(c -> c.add(idpVerificationCertificate))
|
||||
.localEntityIdTemplate(localEntityIdTemplate)
|
||||
.assertionConsumerServiceUrlTemplate(acsUrlTemplate)
|
||||
.build();
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,14 +74,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.saml2Login()
|
||||
.relyingPartyRegistrationRepository(
|
||||
new InMemoryRelyingPartyRegistrationRepository(
|
||||
getSaml2AuthenticationConfiguration()
|
||||
)
|
||||
)
|
||||
.loginProcessingUrl("/sample/jc/saml2/sso/{registrationId}")
|
||||
;
|
||||
.saml2Login();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
+2
-2
@@ -44,7 +44,7 @@ public class SecurityConfigTests {
|
||||
public void filterWhenLoginProcessingUrlIsSetInJavaConfigThenTheFilterHasIt() {
|
||||
FilterChainProxy filterChain = context.getBean(FilterChainProxy.class);
|
||||
Assert.assertNotNull(filterChain);
|
||||
final List<Filter> filters = filterChain.getFilters("/sample/jc/saml2/sso/test-id");
|
||||
final List<Filter> filters = filterChain.getFilters("/login/saml2/sso/one");
|
||||
Assert.assertNotNull(filters);
|
||||
Saml2WebSsoAuthenticationFilter filter = (Saml2WebSsoAuthenticationFilter) filters
|
||||
.stream()
|
||||
@@ -55,6 +55,6 @@ public class SecurityConfigTests {
|
||||
.get();
|
||||
final Object matcher = ReflectionTestUtils.getField(filter, "requiresAuthenticationRequestMatcher");
|
||||
final Object pattern = ReflectionTestUtils.getField(matcher, "pattern");
|
||||
Assert.assertEquals("loginProcessingUrl mismatch", "/sample/jc/saml2/sso/{registrationId}", pattern);
|
||||
Assert.assertEquals("loginProcessingUrl mismatch", "/login/saml2/sso/{registrationId}", pattern);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-9
@@ -142,13 +142,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
+ response
|
||||
+ ". You must use the HttpRequestResponseHolder.response after invoking loadContext");
|
||||
}
|
||||
// saveContext() might already be called by the response wrapper
|
||||
// if something in the chain called sendError() or sendRedirect(). This ensures we
|
||||
// only call it
|
||||
// once per request.
|
||||
if (!responseWrapper.isContextSaved()) {
|
||||
responseWrapper.saveContext(context);
|
||||
}
|
||||
responseWrapper.saveContext(context);
|
||||
}
|
||||
|
||||
public boolean containsContext(HttpServletRequest request) {
|
||||
@@ -305,6 +299,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
private final boolean httpSessionExistedAtStartOfRequest;
|
||||
private final SecurityContext contextBeforeExecution;
|
||||
private final Authentication authBeforeExecution;
|
||||
private boolean isSaveContextInvoked;
|
||||
|
||||
/**
|
||||
* Takes the parameters required to call <code>saveContext()</code> successfully
|
||||
@@ -355,6 +350,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
// SEC-1587 A non-anonymous context may still be in the session
|
||||
// SEC-1735 remove if the contextBeforeExecution was not anonymous
|
||||
httpSession.removeAttribute(springSecurityContextKey);
|
||||
this.isSaveContextInvoked = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -371,7 +367,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
if (contextChanged(context)
|
||||
|| httpSession.getAttribute(springSecurityContextKey) == null) {
|
||||
httpSession.setAttribute(springSecurityContextKey, context);
|
||||
|
||||
this.isSaveContextInvoked = true;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("SecurityContext '" + context
|
||||
+ "' stored to HttpSession: '" + httpSession);
|
||||
@@ -381,7 +377,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
}
|
||||
|
||||
private boolean contextChanged(SecurityContext context) {
|
||||
return context != contextBeforeExecution
|
||||
return this.isSaveContextInvoked || context != contextBeforeExecution
|
||||
|| context.getAuthentication() != authBeforeExecution;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -13,9 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.csrf;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
||||
@@ -28,6 +30,9 @@ import javax.servlet.http.HttpSession;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
@@ -35,8 +40,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Applies
|
||||
@@ -58,6 +61,7 @@ import static java.lang.Boolean.TRUE;
|
||||
* @since 3.2
|
||||
*/
|
||||
public final class CsrfFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* The default {@link RequestMatcher} that indicates if CSRF protection is required or
|
||||
* not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
|
||||
@@ -66,18 +70,21 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();
|
||||
|
||||
/**
|
||||
* The attribute name to use when marking a given request as one that should not be filtered.
|
||||
* The attribute name to use when marking a given request as one that should not be
|
||||
* filtered.
|
||||
*
|
||||
* To use, set the attribute on your {@link HttpServletRequest}:
|
||||
* <pre>
|
||||
* To use, set the attribute on your {@link HttpServletRequest}: <pre>
|
||||
* CsrfFilter.skipRequest(request);
|
||||
* </pre>
|
||||
*/
|
||||
private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfFilter.class.getName();
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final CsrfTokenRepository tokenRepository;
|
||||
|
||||
private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
|
||||
|
||||
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
|
||||
|
||||
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
|
||||
@@ -87,62 +94,46 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||
return TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER));
|
||||
return Boolean.TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet
|
||||
* .http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
|
||||
* javax.servlet.FilterChain)
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
request.setAttribute(HttpServletResponse.class.getName(), response);
|
||||
|
||||
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
|
||||
final boolean missingToken = csrfToken == null;
|
||||
boolean missingToken = (csrfToken == null);
|
||||
if (missingToken) {
|
||||
csrfToken = this.tokenRepository.generateToken(request);
|
||||
this.tokenRepository.saveToken(csrfToken, request, response);
|
||||
}
|
||||
request.setAttribute(CsrfToken.class.getName(), csrfToken);
|
||||
request.setAttribute(csrfToken.getParameterName(), csrfToken);
|
||||
|
||||
if (!this.requireCsrfProtectionMatcher.matches(request)) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not protect against CSRF since request did not match "
|
||||
+ this.requireCsrfProtectionMatcher);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String actualToken = request.getHeader(csrfToken.getHeaderName());
|
||||
if (actualToken == null) {
|
||||
actualToken = request.getParameter(csrfToken.getParameterName());
|
||||
}
|
||||
if (!csrfToken.getToken().equals(actualToken)) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Invalid CSRF token found for "
|
||||
+ UrlUtils.buildFullRequestUrl(request));
|
||||
}
|
||||
if (missingToken) {
|
||||
this.accessDeniedHandler.handle(request, response,
|
||||
new MissingCsrfTokenException(actualToken));
|
||||
}
|
||||
else {
|
||||
this.accessDeniedHandler.handle(request, response,
|
||||
new InvalidCsrfTokenException(csrfToken, actualToken));
|
||||
}
|
||||
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
|
||||
this.logger.debug(
|
||||
LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
|
||||
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
|
||||
: new MissingCsrfTokenException(actualToken);
|
||||
this.accessDeniedHandler.handle(request, response, exception);
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
public static void skipRequest(HttpServletRequest request) {
|
||||
request.setAttribute(SHOULD_NOT_FILTER, TRUE);
|
||||
request.setAttribute(SHOULD_NOT_FILTER, Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,14 +145,11 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
* The default is to apply CSRF protection for any HTTP method other than GET, HEAD,
|
||||
* TRACE, OPTIONS.
|
||||
* </p>
|
||||
*
|
||||
* @param requireCsrfProtectionMatcher the {@link RequestMatcher} used to determine if
|
||||
* CSRF protection should be applied.
|
||||
*/
|
||||
public void setRequireCsrfProtectionMatcher(
|
||||
RequestMatcher requireCsrfProtectionMatcher) {
|
||||
Assert.notNull(requireCsrfProtectionMatcher,
|
||||
"requireCsrfProtectionMatcher cannot be null");
|
||||
public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
|
||||
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
|
||||
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
|
||||
}
|
||||
|
||||
@@ -172,7 +160,6 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
* <p>
|
||||
* The default is to use AccessDeniedHandlerImpl with no arguments.
|
||||
* </p>
|
||||
*
|
||||
* @param accessDeniedHandler the {@link AccessDeniedHandler} to use
|
||||
*/
|
||||
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
|
||||
@@ -180,20 +167,39 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
this.accessDeniedHandler = accessDeniedHandler;
|
||||
}
|
||||
|
||||
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
|
||||
private final HashSet<String> allowedMethods = new HashSet<>(
|
||||
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
|
||||
/**
|
||||
* Constant time comparison to prevent against timing attacks.
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @return
|
||||
*/
|
||||
private static boolean equalsConstantTime(String expected, String actual) {
|
||||
if (expected == actual) {
|
||||
return true;
|
||||
}
|
||||
if (expected == null || actual == null) {
|
||||
return false;
|
||||
}
|
||||
// Encode after ensure that the string is not null
|
||||
byte[] expectedBytes = Utf8.encode(expected);
|
||||
byte[] actualBytes = Utf8.encode(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
|
||||
|
||||
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
|
||||
* servlet.http.HttpServletRequest)
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return !this.allowedMethods.contains(request.getMethod());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CsrfNotRequired " + this.allowedMethods;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ public final class CurrentSecurityContextArgumentResolver
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
context.setRootObject(securityContext);
|
||||
context.setVariable("this", securityContext);
|
||||
|
||||
context.setBeanResolver(this.beanResolver);
|
||||
Expression expression = this.parser.parseExpression(expressionToParse);
|
||||
securityContextResult = expression.getValue(context);
|
||||
}
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* A {@link ServerCsrfTokenRepository} that persists the CSRF token in a cookie named "XSRF-TOKEN" and
|
||||
@@ -62,7 +63,7 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
|
||||
|
||||
@Override
|
||||
public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
|
||||
return Mono.fromCallable(this::createCsrfToken);
|
||||
return Mono.fromCallable(this::createCsrfToken).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 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,26 +16,28 @@
|
||||
|
||||
package org.springframework.security.web.server.csrf;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
|
||||
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -64,13 +66,14 @@ import static java.lang.Boolean.TRUE;
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CsrfWebFilter implements WebFilter {
|
||||
|
||||
public static final ServerWebExchangeMatcher DEFAULT_CSRF_MATCHER = new DefaultRequireCsrfProtectionMatcher();
|
||||
|
||||
/**
|
||||
* The attribute name to use when marking a given request as one that should not be filtered.
|
||||
* The attribute name to use when marking a given request as one that should not be
|
||||
* filtered.
|
||||
*
|
||||
* To use, set the attribute on your {@link ServerWebExchange}:
|
||||
* <pre>
|
||||
* To use, set the attribute on your {@link ServerWebExchange}: <pre>
|
||||
* CsrfWebFilter.skipExchange(exchange);
|
||||
* </pre>
|
||||
*/
|
||||
@@ -80,32 +83,31 @@ public class CsrfWebFilter implements WebFilter {
|
||||
|
||||
private ServerCsrfTokenRepository csrfTokenRepository = new WebSessionServerCsrfTokenRepository();
|
||||
|
||||
private ServerAccessDeniedHandler accessDeniedHandler = new HttpStatusServerAccessDeniedHandler(HttpStatus.FORBIDDEN);
|
||||
private ServerAccessDeniedHandler accessDeniedHandler = new HttpStatusServerAccessDeniedHandler(
|
||||
HttpStatus.FORBIDDEN);
|
||||
|
||||
private boolean isTokenFromMultipartDataEnabled;
|
||||
|
||||
public void setAccessDeniedHandler(
|
||||
ServerAccessDeniedHandler accessDeniedHandler) {
|
||||
public void setAccessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) {
|
||||
Assert.notNull(accessDeniedHandler, "accessDeniedHandler");
|
||||
this.accessDeniedHandler = accessDeniedHandler;
|
||||
}
|
||||
|
||||
public void setCsrfTokenRepository(
|
||||
ServerCsrfTokenRepository csrfTokenRepository) {
|
||||
public void setCsrfTokenRepository(ServerCsrfTokenRepository csrfTokenRepository) {
|
||||
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
|
||||
this.csrfTokenRepository = csrfTokenRepository;
|
||||
}
|
||||
|
||||
public void setRequireCsrfProtectionMatcher(
|
||||
ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
|
||||
public void setRequireCsrfProtectionMatcher(ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
|
||||
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
|
||||
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies if the {@code CsrfWebFilter} should try to resolve the actual CSRF token from the body of multipart
|
||||
* data requests.
|
||||
* @param tokenFromMultipartDataEnabled true if should read from multipart form body, else false. Default is false
|
||||
* Specifies if the {@code CsrfWebFilter} should try to resolve the actual CSRF token
|
||||
* from the body of multipart data requests.
|
||||
* @param tokenFromMultipartDataEnabled true if should read from multipart form body,
|
||||
* else false. Default is false
|
||||
*/
|
||||
public void setTokenFromMultipartDataEnabled(boolean tokenFromMultipartDataEnabled) {
|
||||
this.isTokenFromMultipartDataEnabled = tokenFromMultipartDataEnabled;
|
||||
@@ -113,38 +115,33 @@ public class CsrfWebFilter implements WebFilter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
if (TRUE.equals(exchange.getAttribute(SHOULD_NOT_FILTER))) {
|
||||
if (Boolean.TRUE.equals(exchange.getAttribute(SHOULD_NOT_FILTER))) {
|
||||
return chain.filter(exchange).then(Mono.empty());
|
||||
}
|
||||
|
||||
return this.requireCsrfProtectionMatcher.matches(exchange)
|
||||
.filter( matchResult -> matchResult.isMatch())
|
||||
.filter( matchResult -> !exchange.getAttributes().containsKey(CsrfToken.class.getName()))
|
||||
.flatMap(m -> validateToken(exchange))
|
||||
.flatMap(m -> continueFilterChain(exchange, chain))
|
||||
.switchIfEmpty(continueFilterChain(exchange, chain).then(Mono.empty()))
|
||||
.onErrorResume(CsrfException.class, e -> this.accessDeniedHandler
|
||||
.handle(exchange, e));
|
||||
return this.requireCsrfProtectionMatcher.matches(exchange).filter(MatchResult::isMatch)
|
||||
.filter((matchResult) -> !exchange.getAttributes().containsKey(CsrfToken.class.getName()))
|
||||
.flatMap((m) -> validateToken(exchange)).flatMap((m) -> continueFilterChain(exchange, chain))
|
||||
.switchIfEmpty(continueFilterChain(exchange, chain).then(Mono.empty()))
|
||||
.onErrorResume(CsrfException.class, (ex) -> this.accessDeniedHandler.handle(exchange, ex));
|
||||
}
|
||||
|
||||
public static void skipExchange(ServerWebExchange exchange) {
|
||||
exchange.getAttributes().put(SHOULD_NOT_FILTER, TRUE);
|
||||
exchange.getAttributes().put(SHOULD_NOT_FILTER, Boolean.TRUE);
|
||||
}
|
||||
|
||||
private Mono<Void> validateToken(ServerWebExchange exchange) {
|
||||
return this.csrfTokenRepository.loadToken(exchange)
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new CsrfException("CSRF Token has been associated to this client"))))
|
||||
.filterWhen(expected -> containsValidCsrfToken(exchange, expected))
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new CsrfException("Invalid CSRF Token"))))
|
||||
.then();
|
||||
.switchIfEmpty(
|
||||
Mono.defer(() -> Mono.error(new CsrfException("An expected CSRF token cannot be found"))))
|
||||
.filterWhen((expected) -> containsValidCsrfToken(exchange, expected))
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new CsrfException("Invalid CSRF Token")))).then();
|
||||
}
|
||||
|
||||
private Mono<Boolean> containsValidCsrfToken(ServerWebExchange exchange, CsrfToken expected) {
|
||||
return exchange.getFormData()
|
||||
.flatMap(data -> Mono.justOrEmpty(data.getFirst(expected.getParameterName())))
|
||||
.switchIfEmpty(Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(expected.getHeaderName())))
|
||||
.switchIfEmpty(tokenFromMultipartData(exchange, expected))
|
||||
.map(actual -> actual.equals(expected.getToken()));
|
||||
return exchange.getFormData().flatMap((data) -> Mono.justOrEmpty(data.getFirst(expected.getParameterName())))
|
||||
.switchIfEmpty(Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(expected.getHeaderName())))
|
||||
.switchIfEmpty(tokenFromMultipartData(exchange, expected))
|
||||
.map((actual) -> equalsConstantTime(actual, expected.getToken()));
|
||||
}
|
||||
|
||||
private Mono<String> tokenFromMultipartData(ServerWebExchange exchange, CsrfToken expected) {
|
||||
@@ -157,14 +154,12 @@ public class CsrfWebFilter implements WebFilter {
|
||||
if (!contentType.includes(MediaType.MULTIPART_FORM_DATA)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return exchange.getMultipartData()
|
||||
.map(d -> d.getFirst(expected.getParameterName()))
|
||||
.cast(FormFieldPart.class)
|
||||
.map(FormFieldPart::value);
|
||||
return exchange.getMultipartData().map((d) -> d.getFirst(expected.getParameterName())).cast(FormFieldPart.class)
|
||||
.map(FormFieldPart::value);
|
||||
}
|
||||
|
||||
private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return Mono.defer(() ->{
|
||||
return Mono.defer(() -> {
|
||||
Mono<CsrfToken> csrfToken = csrfToken(exchange);
|
||||
exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
return chain.filter(exchange);
|
||||
@@ -172,26 +167,45 @@ public class CsrfWebFilter implements WebFilter {
|
||||
}
|
||||
|
||||
private Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
|
||||
return this.csrfTokenRepository.loadToken(exchange)
|
||||
.switchIfEmpty(generateToken(exchange));
|
||||
return this.csrfTokenRepository.loadToken(exchange).switchIfEmpty(generateToken(exchange));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant time comparison to prevent against timing attacks.
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @return
|
||||
*/
|
||||
private static boolean equalsConstantTime(String expected, String actual) {
|
||||
if (expected == actual) {
|
||||
return true;
|
||||
}
|
||||
if (expected == null || actual == null) {
|
||||
return false;
|
||||
}
|
||||
// Encode after ensure that the string is not null
|
||||
byte[] expectedBytes = Utf8.encode(expected);
|
||||
byte[] actualBytes = Utf8.encode(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
private Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
|
||||
return this.csrfTokenRepository.generateToken(exchange)
|
||||
.delayUntil(token -> this.csrfTokenRepository.saveToken(exchange, token));
|
||||
.delayUntil((token) -> this.csrfTokenRepository.saveToken(exchange, token));
|
||||
}
|
||||
|
||||
private static class DefaultRequireCsrfProtectionMatcher implements ServerWebExchangeMatcher {
|
||||
|
||||
private static final Set<HttpMethod> ALLOWED_METHODS = new HashSet<>(
|
||||
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE, HttpMethod.OPTIONS));
|
||||
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE, HttpMethod.OPTIONS));
|
||||
|
||||
@Override
|
||||
public Mono<MatchResult> matches(ServerWebExchange exchange) {
|
||||
return Mono.just(exchange.getRequest())
|
||||
.flatMap(r -> Mono.justOrEmpty(r.getMethod()))
|
||||
.filter(m -> ALLOWED_METHODS.contains(m))
|
||||
.flatMap(m -> MatchResult.notMatch())
|
||||
.switchIfEmpty(MatchResult.match());
|
||||
return Mono.just(exchange.getRequest()).flatMap((r) -> Mono.justOrEmpty(r.getMethod()))
|
||||
.filter(ALLOWED_METHODS::contains).flatMap((m) -> MatchResult.notMatch())
|
||||
.switchIfEmpty(MatchResult.match());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+58
@@ -16,11 +16,15 @@
|
||||
|
||||
package org.springframework.security.web.context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -30,6 +34,7 @@ import javax.servlet.http.HttpSession;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
@@ -37,10 +42,14 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Transient;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
@@ -174,6 +183,48 @@ public class HttpSessionSecurityContextRepositoryTests {
|
||||
verify(session).setAttribute(SPRING_SECURITY_CONTEXT_KEY, ctx);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSaveNewContextThenOriginalContextThenOriginalContextSaved() throws Exception {
|
||||
HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository();
|
||||
SecurityContextPersistenceFilter securityContextPersistenceFilter = new SecurityContextPersistenceFilter(
|
||||
repository);
|
||||
|
||||
UserDetails original = User.withUsername("user").password("password").roles("USER").build();
|
||||
SecurityContext originalContext = createSecurityContext(original);
|
||||
UserDetails impersonate = User.withUserDetails(original).username("impersonate").build();
|
||||
SecurityContext impersonateContext = createSecurityContext(impersonate);
|
||||
|
||||
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
|
||||
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
|
||||
|
||||
Filter saveImpersonateContext = (request, response, chain) -> {
|
||||
SecurityContextHolder.setContext(impersonateContext);
|
||||
// ensure the response is committed to trigger save
|
||||
response.flushBuffer();
|
||||
chain.doFilter(request, response);
|
||||
};
|
||||
Filter saveOriginalContext = (request, response, chain) -> {
|
||||
SecurityContextHolder.setContext(originalContext);
|
||||
chain.doFilter(request, response);
|
||||
};
|
||||
HttpServlet servlet = new HttpServlet() {
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
resp.getWriter().write("Hi");
|
||||
}
|
||||
};
|
||||
|
||||
SecurityContextHolder.setContext(originalContext);
|
||||
MockFilterChain chain = new MockFilterChain(servlet, saveImpersonateContext, saveOriginalContext);
|
||||
|
||||
securityContextPersistenceFilter.doFilter(mockRequest, mockResponse, chain);
|
||||
|
||||
assertThat(mockRequest.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY))
|
||||
.isEqualTo(originalContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonSecurityContextInSessionIsIgnored() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
@@ -668,6 +719,13 @@ public class HttpSessionSecurityContextRepositoryTests {
|
||||
assertThat(session).isNull();
|
||||
}
|
||||
|
||||
private SecurityContext createSecurityContext(UserDetails userDetails) {
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails,
|
||||
userDetails.getPassword(), userDetails.getAuthorities());
|
||||
SecurityContext securityContext = new SecurityContextImpl(token);
|
||||
return securityContext;
|
||||
}
|
||||
|
||||
@Transient
|
||||
private static class SomeTransientAuthentication extends AbstractAuthenticationToken {
|
||||
SomeTransientAuthentication() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2021 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,7 @@
|
||||
package org.springframework.security.web.csrf;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -89,6 +90,18 @@ public class CsrfFilterTests {
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullConstantTimeEquals() throws Exception {
|
||||
Method method = CsrfFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
|
||||
method.setAccessible(true);
|
||||
assertThat(method.invoke(CsrfFilter.class, null, null)).isEqualTo(true);
|
||||
String expectedToken = "Hello—World";
|
||||
String actualToken = new String("Hello—World");
|
||||
assertThat(method.invoke(CsrfFilter.class, expectedToken, null)).isEqualTo(false);
|
||||
assertThat(method.invoke(CsrfFilter.class, expectedToken, "hello-world")).isEqualTo(false);
|
||||
assertThat(method.invoke(CsrfFilter.class, expectedToken, actualToken)).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorNullRepository() {
|
||||
new CsrfFilter(null);
|
||||
|
||||
+30
@@ -27,6 +27,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
@@ -35,17 +36,28 @@ import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
import static org.mockito.BDDMockito.when;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
public class AuthenticationPrincipalArgumentResolverTests {
|
||||
|
||||
private BeanResolver beanResolver;
|
||||
|
||||
private Object expectedPrincipal;
|
||||
private AuthenticationPrincipalArgumentResolver resolver;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
beanResolver = mock(BeanResolver.class);
|
||||
resolver = new AuthenticationPrincipalArgumentResolver();
|
||||
resolver.setBeanResolver(this.beanResolver);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -128,6 +140,17 @@ public class AuthenticationPrincipalArgumentResolverTests {
|
||||
.isEqualTo(this.expectedPrincipal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentSpelBean() throws Exception {
|
||||
CustomUserPrincipal principal = new CustomUserPrincipal();
|
||||
setAuthenticationPrincipal(principal);
|
||||
when(this.beanResolver.resolve(any(), eq("test"))).thenReturn(principal.property);
|
||||
this.expectedPrincipal = principal.property;
|
||||
assertThat(this.resolver.resolveArgument(showUserSpelBean(), null, null, null))
|
||||
.isEqualTo(this.expectedPrincipal);
|
||||
verify(this.beanResolver).resolve(any(), eq("test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentSpelCopy() throws Exception {
|
||||
CopyUserPrincipal principal = new CopyUserPrincipal("property");
|
||||
@@ -198,6 +221,10 @@ public class AuthenticationPrincipalArgumentResolverTests {
|
||||
return getMethodParameter("showUserSpel", String.class);
|
||||
}
|
||||
|
||||
private MethodParameter showUserSpelBean() {
|
||||
return getMethodParameter("showUserSpelBean", String.class);
|
||||
}
|
||||
|
||||
private MethodParameter showUserSpelCopy() {
|
||||
return getMethodParameter("showUserSpelCopy", CopyUserPrincipal.class);
|
||||
}
|
||||
@@ -255,6 +282,9 @@ public class AuthenticationPrincipalArgumentResolverTests {
|
||||
@AuthenticationPrincipal(expression = "property") String user) {
|
||||
}
|
||||
|
||||
public void showUserSpelBean(@AuthenticationPrincipal(expression = "@test") String user) {
|
||||
}
|
||||
|
||||
public void showUserSpelCopy(
|
||||
@AuthenticationPrincipal(expression = "new org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolverTests$CopyUserPrincipal(#this)") CopyUserPrincipal user) {
|
||||
}
|
||||
|
||||
+28
@@ -26,6 +26,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -38,6 +39,11 @@ import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
import static org.mockito.BDDMockito.when;
|
||||
|
||||
/**
|
||||
* @author Dan Zheng
|
||||
@@ -45,11 +51,16 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*
|
||||
*/
|
||||
public class CurrentSecurityContextArgumentResolverTests {
|
||||
|
||||
private BeanResolver beanResolver;
|
||||
|
||||
private CurrentSecurityContextArgumentResolver resolver;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.beanResolver = mock(BeanResolver.class);
|
||||
this.resolver = new CurrentSecurityContextArgumentResolver();
|
||||
this.resolver.setBeanResolver(this.beanResolver);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -104,6 +115,15 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
assertThat(auth1.getPrincipal()).isEqualTo(principal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithAuthenticationWithBean() throws Exception {
|
||||
String principal = "john";
|
||||
when(this.beanResolver.resolve(any(), eq("test"))).thenReturn(principal);
|
||||
assertThat(this.resolver.resolveArgument(showSecurityContextAuthenticationWithBean(), null, null, null))
|
||||
.isEqualTo(principal);
|
||||
verify(this.beanResolver).resolve(any(), eq("test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveArgumentWithNullAuthentication() {
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
@@ -217,6 +237,10 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
return getMethodParameter("showSecurityContextAuthenticationAnnotation", Authentication.class);
|
||||
}
|
||||
|
||||
public MethodParameter showSecurityContextAuthenticationWithBean() {
|
||||
return getMethodParameter("showSecurityContextAuthenticationWithBean", String.class);
|
||||
}
|
||||
|
||||
private MethodParameter showSecurityContextAuthenticationWithOptionalPrincipal() {
|
||||
return getMethodParameter("showSecurityContextAuthenticationWithOptionalPrincipal", Object.class);
|
||||
}
|
||||
@@ -279,6 +303,10 @@ public class CurrentSecurityContextArgumentResolverTests {
|
||||
public void showSecurityContextAuthenticationAnnotation(@CurrentSecurityContext(expression = "authentication") Authentication authentication) {
|
||||
}
|
||||
|
||||
public void showSecurityContextAuthenticationWithBean(
|
||||
@CurrentSecurityContext(expression = "@test") String name) {
|
||||
}
|
||||
|
||||
public void showSecurityContextAuthenticationWithOptionalPrincipal(@CurrentSecurityContext(expression = "authentication?.principal") Object principal) {
|
||||
}
|
||||
|
||||
|
||||
+18
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 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.web.server.csrf;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
@@ -65,8 +67,19 @@ public class CsrfWebFilterTests {
|
||||
private MockServerWebExchange get = from(
|
||||
MockServerHttpRequest.get("/"));
|
||||
|
||||
private ServerWebExchange post = from(
|
||||
MockServerHttpRequest.post("/"));
|
||||
private MockServerWebExchange post = MockServerWebExchange.from(MockServerHttpRequest.post("/"));
|
||||
|
||||
@Test
|
||||
public void nullConstantTimeEquals() throws Exception {
|
||||
Method method = CsrfWebFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
|
||||
method.setAccessible(true);
|
||||
assertThat(method.invoke(CsrfWebFilter.class, null, null)).isEqualTo(true);
|
||||
String expectedToken = "Hello—World";
|
||||
String actualToken = new String("Hello—World");
|
||||
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, null)).isEqualTo(false);
|
||||
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, "hello-world")).isEqualTo(false);
|
||||
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, actualToken)).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenGetThenSessionNotCreatedAndChainContinues() {
|
||||
@@ -110,6 +123,8 @@ public class CsrfWebFilterTests {
|
||||
.verifyComplete();
|
||||
|
||||
assertThat(this.post.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
StepVerifier.create(this.post.getResponse().getBodyAsString())
|
||||
.assertNext(b -> assertThat(b).contains("An expected CSRF token cannot be found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user