Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e41360b4a3 | |||
| e2e06539fc | |||
| 93f59a228b | |||
| b2bb014492 | |||
| 062910c2c4 | |||
| 40fdb5a60d | |||
| 2fa0da5cde | |||
| c5f4ae5c0a | |||
| c68c1c55ff | |||
| be2595bc66 | |||
| dddd71439f | |||
| 95a1ccb52c | |||
| 4060f83261 | |||
| f9a7ae0a6f | |||
| 425d008f1d | |||
| b73104c9d1 | |||
| 895ef6cf76 | |||
| cb4bb463da | |||
| 5fd81eeaf1 | |||
| a332e2a728 | |||
| 9daf058a6e | |||
| 839cc5e851 | |||
| 0087128be8 | |||
| bfc9c5e18f | |||
| 6fbd038111 | |||
| 700bda68b7 | |||
| 53870ab3de | |||
| fe13b488a1 | |||
| c38495bfaf | |||
| c4b210b580 | |||
| 1a28b4f849 | |||
| 63cd52d1a6 | |||
| 895ae0ac55 | |||
| e6c268add0 | |||
| 82d471a592 | |||
| f48a006034 | |||
| b7a0959ede | |||
| 29f4193529 | |||
| ab59adbfd2 | |||
| de4b3a4310 | |||
| e691906dbc | |||
| 5a06fbd984 | |||
| 34b55242fe | |||
| 26e34368c0 | |||
| 0fb7c24cbb | |||
| 0351883dfa | |||
| 95132d9fe3 | |||
| bf9f1de6e1 | |||
| 8c0248d0dc | |||
| df4d0dc488 | |||
| 073f72277e |
@@ -2,8 +2,6 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
@@ -26,16 +24,6 @@ jobs:
|
||||
outputs:
|
||||
runjobs: ${{ steps.continue.outputs.runjobs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Initiate error tracking
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
with:
|
||||
job-name: "prerequisites"
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: errors
|
||||
path: job-prerequisites.txt
|
||||
- id: continue
|
||||
name: Determine if should continue
|
||||
if: env.RUN_JOBS == 'true'
|
||||
@@ -43,7 +31,10 @@ jobs:
|
||||
build_jdk_11:
|
||||
name: Build JDK 11
|
||||
needs: [prerequisites]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -61,59 +52,11 @@ jobs:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
build_windows:
|
||||
name: Build Windows
|
||||
needs: [prerequisites]
|
||||
runs-on: windows-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '11'
|
||||
- name: Setup gradle user name
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
set GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
set GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
set GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
snapshot_tests:
|
||||
name: Test against snapshots
|
||||
needs: [prerequisites]
|
||||
@@ -135,17 +78,6 @@ jobs:
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion='20+' -PspringDataVersion='Neumann-BUILD-SNAPSHOT' -PrsocketVersion=1.1.0-SNAPSHOT -PspringBootVersion=2.4.0-SNAPSHOT -PlocksDisabled --stacktrace
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
sonar_analysis:
|
||||
name: Static Code Analysis
|
||||
needs: [prerequisites]
|
||||
@@ -179,20 +111,9 @@ jobs:
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew sonarqube -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PexcludeProjects='**/samples/**' -Dsonar.host.url="$SONAR_URL" -Dsonar.login="$SONAR_TOKEN" --stacktrace
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
deploy_artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis]
|
||||
needs: [build_jdk_11, snapshot_tests, sonar_analysis]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -209,31 +130,17 @@ jobs:
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
export VERSION_HEADER=$'Version: GnuPG v2\n\n'
|
||||
export ORG_GRADLE_PROJECT_signingKey=${GPG_PRIVATE_KEY_NO_HEADER#"$VERSION_HEADER"}
|
||||
export ORG_GRADLE_PROJECT_signingPassword="$GPG_PASSPHRASE"
|
||||
./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
|
||||
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
|
||||
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
|
||||
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
deploy_docs:
|
||||
name: Deploy Docs
|
||||
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis]
|
||||
needs: [build_jdk_11, snapshot_tests, sonar_analysis]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -255,20 +162,9 @@ jobs:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
DOCS_HOST: ${{ secrets.DOCS_HOST }}
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
deploy_schema:
|
||||
name: Deploy Schema
|
||||
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis]
|
||||
needs: [build_jdk_11, snapshot_tests, sonar_analysis]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -290,34 +186,16 @@ jobs:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
DOCS_HOST: ${{ secrets.DOCS_HOST }}
|
||||
- name: Track error step
|
||||
uses: spring-projects/track-build-errors-action@v1
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
job-name: ${{ github.job }}
|
||||
- name: Export errors file
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: errors
|
||||
path: job-${{ github.job }}.txt
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis, deploy_artifacts, deploy_docs, deploy_schema]
|
||||
if: always()
|
||||
needs: [build_jdk_11, snapshot_tests, sonar_analysis, deploy_artifacts, deploy_docs, deploy_schema]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download errors folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: errors
|
||||
- name: Send Slack message
|
||||
uses: spring-projects/notify-slack-errors-action@v1
|
||||
uses: Gamesight/slack-workflow-status@v1.0.1
|
||||
with:
|
||||
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
branch-name: ${{ github.ref }}
|
||||
commit-sha: ${{ github.sha }}
|
||||
commit-owner: ${{ github.actor }}
|
||||
repo-name: ${{ github.repository }}
|
||||
run-id: ${{ github.run_id }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Be sure to read the https://docs.spring.io/spring-security/site/docs/current/ref
|
||||
Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
|
||||
|
||||
== Quick Start
|
||||
We recommend you visit https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/[Spring Security Reference] and read the "Getting Started" page.
|
||||
See https://docs.spring.io/spring-security/site/docs/5.5.x/reference/html5/#servlet-hello[Hello Spring Security] to get started with a "Hello, World" application.
|
||||
|
||||
== Building from Source
|
||||
Spring Security uses a https://gradle.org[Gradle]-based build system.
|
||||
|
||||
@@ -68,12 +68,6 @@ updateDependenciesSettings {
|
||||
milestoneVersions()
|
||||
snapshotVersions()
|
||||
addRule { components ->
|
||||
components.withModule("commons-codec:commons-codec") { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("commons-codec updates break saml tests");
|
||||
}
|
||||
}
|
||||
components.withModule("org.python:jython") { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
|
||||
@@ -80,6 +80,7 @@ dependencies {
|
||||
implementation 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
|
||||
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.8'
|
||||
implementation 'org.aim42:htmlSanityCheck:1.1.6'
|
||||
implementation 'org.asciidoctor:asciidoctor-gradle-jvm:3.1.0'
|
||||
implementation 'org.asciidoctor:asciidoctor-gradle-jvm-pdf:3.1.0'
|
||||
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
|
||||
|
||||
@@ -36,6 +36,9 @@ class ArtifactoryPlugin implements Plugin<Project> {
|
||||
password = artifactoryPassword
|
||||
}
|
||||
}
|
||||
defaults {
|
||||
publications('mavenJava')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package io.spring.gradle.convention
|
||||
|
||||
import org.aim42.htmlsanitycheck.HtmlSanityCheckPlugin
|
||||
import org.aim42.htmlsanitycheck.HtmlSanityCheckTask
|
||||
import org.aim42.htmlsanitycheck.check.BrokenHttpLinksChecker
|
||||
import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Configuration
|
||||
import org.gradle.api.artifacts.DependencySet
|
||||
import org.gradle.api.plugins.PluginManager
|
||||
import org.gradle.api.tasks.Sync
|
||||
import org.gradle.api.tasks.bundling.Zip
|
||||
|
||||
/**
|
||||
@@ -25,6 +24,7 @@ public class DocsPlugin implements Plugin<Project> {
|
||||
pluginManager.apply(AsciidoctorConventionPlugin);
|
||||
pluginManager.apply(DeployDocsPlugin);
|
||||
pluginManager.apply(JavadocApiPlugin);
|
||||
pluginManager.apply(HtmlSanityCheckPlugin)
|
||||
|
||||
String projectName = Utils.getProjectName(project);
|
||||
String pdfFilename = projectName + "-reference.pdf";
|
||||
@@ -38,6 +38,17 @@ public class DocsPlugin implements Plugin<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks.withType(HtmlSanityCheckTask) { HtmlSanityCheckTask t ->
|
||||
project.configure(t) {
|
||||
t.dependsOn 'asciidoctor'
|
||||
t.checkerClasses = [BrokenHttpLinksChecker]
|
||||
t.checkingResultsDir = new File(project.getBuildDir(), "/report/htmlchecks")
|
||||
t.failOnErrors = false
|
||||
t.httpConnectionTimeout = 3000
|
||||
t.sourceDir = new File(project.getBuildDir(), "/docs/asciidoc/")
|
||||
t.sourceDocuments = project.files(new File(project.getBuildDir(), "/docs/asciidoc/index.html"))
|
||||
}
|
||||
}
|
||||
|
||||
Task docsZip = project.tasks.create('docsZip', Zip) {
|
||||
dependsOn 'api', 'asciidoctor'
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.spring.gradle.testkit.junit.rules.TestKit
|
||||
import org.gradle.testkit.runner.BuildResult
|
||||
import org.gradle.testkit.runner.TaskOutcome
|
||||
import org.junit.Rule
|
||||
import spock.lang.Ignore
|
||||
import spock.lang.Specification
|
||||
|
||||
class ShowcaseITest extends Specification {
|
||||
@@ -36,6 +37,7 @@ class ShowcaseITest extends Specification {
|
||||
result.output.contains("BUILD SUCCESSFUL")
|
||||
}
|
||||
|
||||
@Ignore
|
||||
def "install"() {
|
||||
when:
|
||||
BuildResult result = testKit.withProjectResource("samples/showcase/")
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.spring.gradle.convention
|
||||
import io.spring.gradle.testkit.junit.rules.TestKit
|
||||
import org.gradle.testkit.runner.BuildResult
|
||||
import org.junit.Rule
|
||||
import spock.lang.Ignore
|
||||
import spock.lang.Specification
|
||||
|
||||
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
|
||||
@@ -27,6 +28,7 @@ class SpringMavenPluginITest extends Specification {
|
||||
|
||||
@Rule final TestKit testKit = new TestKit()
|
||||
|
||||
@Ignore
|
||||
def "install"() {
|
||||
when:
|
||||
BuildResult result = testKit.withProjectResource("samples/maven/install")
|
||||
@@ -46,6 +48,7 @@ class SpringMavenPluginITest extends Specification {
|
||||
</dependency>""".replaceAll('\\s',''))
|
||||
}
|
||||
|
||||
@Ignore
|
||||
def "signArchives when in memory"() {
|
||||
when:
|
||||
BuildResult result = testKit.withProjectResource("samples/maven/signing")
|
||||
|
||||
@@ -100,10 +100,10 @@ rncToXsd {
|
||||
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
apiVersion = "1.4"
|
||||
languageVersion = "1.3"
|
||||
apiVersion = "1.3"
|
||||
freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings"]
|
||||
jvmTarget = "1.8"
|
||||
languageVersion = "1.4"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -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.
|
||||
@@ -114,8 +114,18 @@ final class FilterOrderRegistration {
|
||||
put(SwitchUserFilter.class, order.next());
|
||||
}
|
||||
|
||||
private void put(Class<? extends Filter> filter, int position) {
|
||||
/**
|
||||
* Register a {@link Filter} with its specific position. If the {@link Filter} was
|
||||
* already registered before, the position previously defined is not going to be
|
||||
* overriden
|
||||
* @param filter the {@link Filter} to register
|
||||
* @param position the position to associate with the {@link Filter}
|
||||
*/
|
||||
void put(Class<? extends Filter> filter, int position) {
|
||||
String className = filter.getName();
|
||||
if (this.filterToOrder.containsKey(className)) {
|
||||
return;
|
||||
}
|
||||
this.filterToOrder.put(className, position);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -2653,6 +2653,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
|
||||
int order = this.filterOrders.getOrder(registeredFilter) + offset;
|
||||
this.filters.add(new OrderedFilter(filter, order));
|
||||
this.filterOrders.put(filter.getClass(), order);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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.
|
||||
@@ -97,7 +97,10 @@ public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
if (this.loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) {
|
||||
this.loginPageGeneratingFilter = postProcess(this.loginPageGeneratingFilter);
|
||||
http.addFilter(this.loginPageGeneratingFilter);
|
||||
http.addFilter(this.logoutPageGeneratingFilter);
|
||||
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
|
||||
if (logoutConfigurer != null) {
|
||||
http.addFilter(this.logoutPageGeneratingFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -2223,7 +2223,10 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
if (loginPage != null) {
|
||||
http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
|
||||
http.addFilterAt(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
|
||||
if (http.logout != null) {
|
||||
http.addFilterAt(new LogoutPageGeneratingWebFilter(),
|
||||
SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.builders;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class FilterOrderRegistrationTests {
|
||||
|
||||
private final FilterOrderRegistration filterOrderRegistration = new FilterOrderRegistration();
|
||||
|
||||
@Test
|
||||
public void putWhenNewFilterThenInsertCorrect() {
|
||||
int position = 153;
|
||||
this.filterOrderRegistration.put(MyFilter.class, position);
|
||||
Integer order = this.filterOrderRegistration.getOrder(MyFilter.class);
|
||||
assertThat(order).isEqualTo(position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void putWhenCustomFilterAlreadyExistsThenDoesNotOverride() {
|
||||
int position = 160;
|
||||
this.filterOrderRegistration.put(MyFilter.class, position);
|
||||
this.filterOrderRegistration.put(MyFilter.class, 173);
|
||||
Integer order = this.filterOrderRegistration.getOrder(MyFilter.class);
|
||||
assertThat(order).isEqualTo(position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void putWhenPredefinedFilterThenDoesNotOverride() {
|
||||
int position = 100;
|
||||
Integer predefinedFilterOrderBefore = this.filterOrderRegistration.getOrder(ChannelProcessingFilter.class);
|
||||
this.filterOrderRegistration.put(MyFilter.class, position);
|
||||
Integer myFilterOrder = this.filterOrderRegistration.getOrder(MyFilter.class);
|
||||
Integer predefinedFilterOrderAfter = this.filterOrderRegistration.getOrder(ChannelProcessingFilter.class);
|
||||
assertThat(myFilterOrder).isEqualTo(position);
|
||||
assertThat(predefinedFilterOrderAfter).isEqualTo(predefinedFilterOrderBefore).isEqualTo(position);
|
||||
}
|
||||
|
||||
static class MyFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+144
-1
@@ -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.
|
||||
@@ -30,14 +30,18 @@ import org.assertj.core.api.ListAssert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -70,6 +74,46 @@ public class HttpSecurityAddFilterTest {
|
||||
ExceptionTranslationFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterAfterWhenAfterCustomFilterThenOrderCorrect() {
|
||||
this.spring.register(MyOtherFilterRelativeToMyFilterAfterConfig.class).autowire();
|
||||
|
||||
assertThatFilters().containsSubsequence(WebAsyncManagerIntegrationFilter.class, MyFilter.class,
|
||||
MyOtherFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterBeforeWhenBeforeCustomFilterThenOrderCorrect() {
|
||||
this.spring.register(MyOtherFilterRelativeToMyFilterBeforeConfig.class).autowire();
|
||||
|
||||
assertThatFilters().containsSubsequence(MyOtherFilter.class, MyFilter.class,
|
||||
WebAsyncManagerIntegrationFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterAtWhenAtCustomFilterThenOrderCorrect() {
|
||||
this.spring.register(MyOtherFilterRelativeToMyFilterAtConfig.class).autowire();
|
||||
|
||||
assertThatFilters().containsSubsequence(WebAsyncManagerIntegrationFilter.class, MyFilter.class,
|
||||
MyOtherFilter.class, SecurityContextPersistenceFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterBeforeWhenCustomFilterDifferentPlacesThenOrderCorrect() {
|
||||
this.spring.register(MyOtherFilterBeforeToMyFilterMultipleAfterConfig.class).autowire();
|
||||
|
||||
assertThatFilters().containsSubsequence(WebAsyncManagerIntegrationFilter.class, MyOtherFilter.class,
|
||||
MyFilter.class, ExceptionTranslationFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterBeforeAndAfterWhenCustomFiltersDifferentPlacesThenOrderCorrect() {
|
||||
this.spring.register(MyAnotherFilterRelativeToMyCustomFiltersMultipleConfig.class).autowire();
|
||||
|
||||
assertThatFilters().containsSubsequence(HeaderWriterFilter.class, MyFilter.class, MyOtherFilter.class,
|
||||
MyOtherFilter.class, MyAnotherFilter.class, MyFilter.class, ExceptionTranslationFilter.class);
|
||||
}
|
||||
|
||||
private ListAssert<Class<?>> assertThatFilters() {
|
||||
FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class);
|
||||
List<Class<?>> filters = filterChain.getFilters("/").stream().map(Object::getClass)
|
||||
@@ -87,6 +131,26 @@ public class HttpSecurityAddFilterTest {
|
||||
|
||||
}
|
||||
|
||||
static class MyOtherFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MyAnotherFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyFilterMultipleAfterConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@@ -129,4 +193,83 @@ public class HttpSecurityAddFilterTest {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterRelativeToMyFilterAfterConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAfter(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
|
||||
.addFilterAfter(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterRelativeToMyFilterBeforeConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterBefore(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
|
||||
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterRelativeToMyFilterAtConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAt(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
|
||||
.addFilterAt(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterBeforeToMyFilterMultipleAfterConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAfter(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
|
||||
.addFilterAfter(new MyFilter(), ExceptionTranslationFilter.class)
|
||||
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyAnotherFilterRelativeToMyCustomFiltersMultipleConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAfter(new MyFilter(), HeaderWriterFilter.class)
|
||||
.addFilterBefore(new MyOtherFilter(), ExceptionTranslationFilter.class)
|
||||
.addFilterAfter(new MyOtherFilter(), MyFilter.class)
|
||||
.addFilterAt(new MyAnotherFilter(), MyOtherFilter.class)
|
||||
.addFilterAfter(new MyFilter(), MyAnotherFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
-1
@@ -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.
|
||||
@@ -46,11 +46,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultLoginPageConfigurer}
|
||||
@@ -375,6 +378,18 @@ public class DefaultLoginPageConfigurerTests {
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formLoginWhenLogoutEnabledThenCreatesDefaultLogoutPage() throws Exception {
|
||||
this.spring.register(DefaultLogoutPageConfig.class).autowire();
|
||||
this.mvc.perform(get("/logout").with(user("user"))).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formLoginWhenLogoutDisabledThenDefaultLogoutPageDoesNotExist() throws Exception {
|
||||
this.spring.register(LogoutDisabledConfig.class).autowire();
|
||||
this.mvc.perform(get("/logout").with(user("user"))).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class DefaultLoginPageConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@@ -533,6 +548,41 @@ public class DefaultLoginPageConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class DefaultLogoutPageConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(withDefaults());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class LogoutDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(withDefaults())
|
||||
.logout((logout) -> logout
|
||||
.disable()
|
||||
);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ReflectingObjectPostProcessor implements ObjectPostProcessor<Object> {
|
||||
|
||||
@Override
|
||||
|
||||
+18
-5
@@ -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.
|
||||
@@ -26,7 +26,10 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
@@ -146,7 +149,7 @@ public class LogoutSpecTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenDisabledThenPostToLogoutDoesNothing() {
|
||||
public void logoutWhenDisabledThenDefaultLogoutPageDoesNotExist() {
|
||||
// @formatter:off
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.authorizeExchange()
|
||||
@@ -156,7 +159,7 @@ public class LogoutSpecTests {
|
||||
.logout().disable()
|
||||
.build();
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.bindToControllerAndWebFilters(HomeController.class, securityWebFilter)
|
||||
.build();
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
@@ -171,8 +174,8 @@ public class LogoutSpecTests {
|
||||
.submit(FormLoginTests.HomePage.class);
|
||||
// @formatter:on
|
||||
homePage.assertAt();
|
||||
FormLoginTests.DefaultLogoutPage.to(driver).assertAt().logout();
|
||||
homePage.assertAt();
|
||||
FormLoginTests.DefaultLogoutPage.to(driver);
|
||||
assertThat(driver.getPageSource()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -208,4 +211,14 @@ public class LogoutSpecTests {
|
||||
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class).assertAt();
|
||||
}
|
||||
|
||||
@RestController
|
||||
public static class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String ok() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,23 +2,15 @@ import java.util.concurrent.Callable
|
||||
|
||||
apply plugin: 'io.spring.convention.spring-module'
|
||||
|
||||
def includeProject = project(':spring-security-crypto')
|
||||
|
||||
configurations {
|
||||
included
|
||||
api.extendsFrom included
|
||||
}
|
||||
|
||||
dependencies {
|
||||
management platform(project(":spring-security-dependencies"))
|
||||
api project(':spring-security-crypto')
|
||||
api 'org.springframework:spring-aop'
|
||||
api 'org.springframework:spring-beans'
|
||||
api 'org.springframework:spring-context'
|
||||
api 'org.springframework:spring-core'
|
||||
api 'org.springframework:spring-expression'
|
||||
|
||||
included includeProject
|
||||
|
||||
optional 'com.fasterxml.jackson.core:jackson-databind'
|
||||
optional 'io.projectreactor:reactor-core'
|
||||
optional 'javax.annotation:jsr250-api'
|
||||
@@ -49,12 +41,6 @@ tasks.processResources {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jar.from { includeProject.sourceSets.main.output }
|
||||
|
||||
project.tasks.matching { t -> t.name == "sourcesJar"}.configureEach {
|
||||
from {includeProject.sourceSets.main.java }
|
||||
}
|
||||
|
||||
configure(project.tasks.withType(Test)) {
|
||||
doFirst {
|
||||
systemProperties['springSecurityVersion'] = version
|
||||
|
||||
+10
-10
@@ -7,9 +7,9 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:5.3.7")
|
||||
api platform("org.springframework:spring-framework-bom:5.3.8")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.7")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.0")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.1")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.0.1")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.0")
|
||||
@@ -17,8 +17,8 @@ dependencies {
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.3"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.8.1"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.3.3"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.10"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.9"
|
||||
api "com.squareup.okhttp3:mockwebserver:3.14.9"
|
||||
api "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
api "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
@@ -49,10 +49,10 @@ dependencies {
|
||||
api "org.assertj:assertj-core:3.19.0"
|
||||
api "org.bouncycastle:bcpkix-jdk15on:1.68"
|
||||
api "org.bouncycastle:bcprov-jdk15on:1.68"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.40.v20210413"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.40.v20210413"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.42.v20210604"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.42.v20210604"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hibernate:hibernate-entitymanager:5.4.31.Final"
|
||||
api "org.hibernate:hibernate-entitymanager:5.4.32.Final"
|
||||
api "org.hsqldb:hsqldb:2.6.0"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.2"
|
||||
api "org.mockito:mockito-core:3.9.0"
|
||||
@@ -71,9 +71,9 @@ dependencies {
|
||||
api "org.seleniumhq.selenium:selenium-java:3.141.59"
|
||||
api "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
api "org.skyscreamer:jsonassert:1.5.0"
|
||||
api "org.slf4j:jcl-over-slf4j:1.7.30"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.30"
|
||||
api "org.slf4j:slf4j-api:1.7.30"
|
||||
api "org.slf4j:jcl-over-slf4j:1.7.31"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.31"
|
||||
api "org.slf4j:slf4j-api:1.7.31"
|
||||
api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE"
|
||||
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ asciidoctor {
|
||||
baseDir = file('src/docs/asciidoc')
|
||||
options eruby: 'erubis'
|
||||
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$ghTag"
|
||||
attributes copycss : '',
|
||||
icons : 'font',
|
||||
'source-highlighter': 'prettify',
|
||||
@@ -15,9 +17,10 @@ asciidoctor {
|
||||
idseparator: '-',
|
||||
doctype: 'book',
|
||||
'spring-security-version' : project.version,
|
||||
'download-url' : getDownloadUrl(),
|
||||
'include-maven-repository' : getMavenRepositoryInclude(),
|
||||
revnumber : project.version
|
||||
revnumber : project.version,
|
||||
'gh-samples-url': ghSamplesUrl,
|
||||
'gh-old-samples-url': "https://github.com/spring-projects/spring-security/tree/5.4.x/samples"
|
||||
}
|
||||
|
||||
ext.spec = copySpec {
|
||||
@@ -27,11 +30,6 @@ ext.spec = copySpec {
|
||||
}
|
||||
}
|
||||
|
||||
def getDownloadUrl() {
|
||||
snapshotBuild ? "https://github.com/spring-projects/spring-security/archive/main.zip" : "https://github.com/spring-projects/spring-security/archive/${project.version}.zip"
|
||||
}
|
||||
|
||||
|
||||
def getMavenRepositoryInclude() {
|
||||
if(snapshotBuild) {
|
||||
return "_includes/maven-repository-snapshot.asc"
|
||||
|
||||
@@ -12,11 +12,16 @@ download-url: the URL to download the Spring Security distribution
|
||||
|
||||
== Setting up the sample
|
||||
|
||||
This section outlines how to setup a workspace within https://spring.io/tools/sts[Spring Tool Suite (STS)] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
|
||||
This section outlines how to setup a workspace within https://spring.io/tools[Eclipse IDE with Spring Tools] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
|
||||
|
||||
=== Obtaining the sample project
|
||||
|
||||
Extract the {download-url}[Spring Security Distribution] to a known location and remember it as _SPRING_SECURITY_HOME_.
|
||||
Clone the {gh-samples-url}[Spring Security Samples project] to a known location and remember it as _SPRING_SECURITY_HOME_.
|
||||
[NOTE]
|
||||
====
|
||||
These samples are being migrated to a separate project, however, you can still find
|
||||
the not migrated samples in an older branch of the {gh-old-samples-url}[Spring Security repository].
|
||||
====
|
||||
|
||||
=== Import the {starter-appname} sample application
|
||||
|
||||
|
||||
@@ -12,11 +12,16 @@ download-url: the URL to download the Spring Security distribution
|
||||
|
||||
== Setting up the sample
|
||||
|
||||
This section outlines how to setup a workspace within https://spring.io/tools/sts[Spring Tool Suite (STS)] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
|
||||
This section outlines how to setup a workspace within https://spring.io/tools[Eclipse IDE with Spring Tools] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
|
||||
|
||||
=== Obtaining the sample project
|
||||
|
||||
Extract the {download-url}[Spring Security Distribution] to a known location and remember it as _SPRING_SECURITY_HOME_.
|
||||
Clone the {gh-samples-url}[Spring Security Samples project] to a known location and remember it as _SPRING_SECURITY_HOME_.
|
||||
[NOTE]
|
||||
====
|
||||
These samples are being migrated to a separate project, however, you can still find
|
||||
the not migrated samples in an older branch of the {gh-old-samples-url}[Spring Security repository].
|
||||
====
|
||||
|
||||
=== Import the {starter-appname} sample application
|
||||
|
||||
|
||||
@@ -14,13 +14,27 @@ asciidoctor {
|
||||
}
|
||||
|
||||
asciidoctorj {
|
||||
def docsTag = snapshotBuild ? 'current' : project.version
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
|
||||
def ghOldSamplesUrl = "https://github.com/spring-projects/spring-security/tree/5.4.x/samples"
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$ghTag"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def oldSecurityApiUrl = "https://docs.spring.io/spring-security/site/docs/5.4.x/api/"
|
||||
def springVersion = "5.3.x"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/"
|
||||
attributes 'spring-security-version' : project.version,
|
||||
'spring-boot-version' : springBootVersion,
|
||||
revnumber : project.version,
|
||||
'gh-url': ghUrl,
|
||||
'gh-samples-url': "$ghUrl/samples"
|
||||
'gh-samples-url': ghSamplesUrl,
|
||||
'gh-old-samples-url': ghOldSamplesUrl,
|
||||
'old-security-api-url': oldSecurityApiUrl,
|
||||
'security-api-url': securityApiUrl,
|
||||
'security-reference-url': securityReferenceUrl,
|
||||
'spring-framework-api-url': springFrameworkApiUrl
|
||||
attributeProvider resolvedVersions(project.configurations.testRuntimeClasspath)
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ This means that requests with the HTTP method `GET`, `HEAD`, `OPTIONS`, and `TRA
|
||||
|
||||
[[csrf-protection-stp]]
|
||||
=== Synchronizer Token Pattern
|
||||
The predominant and most comprehensive way to protect against CSRF attacks is to use the https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern[Synchronizer Token Pattern].
|
||||
The predominant and most comprehensive way to protect against CSRF attacks is to use the https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern[Synchronizer Token Pattern].
|
||||
This solution is to ensure that each HTTP request requires, in addition to our session cookie, a secure random generated value called a CSRF token must be present in the HTTP request.
|
||||
|
||||
When an HTTP request is submitted, the server must look up the expected CSRF token and compare it against the actual CSRF token in the HTTP request.
|
||||
@@ -348,7 +348,7 @@ This allows the expected CSRF token to outlive the session.
|
||||
One might ask why the expected CSRF token isn't stored in a cookie by default.
|
||||
This is because there are known exploits in which headers (for example, to specify the cookies) can be set by another domain.
|
||||
This is the same reason Ruby on Rails https://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails/[no longer skips CSRF checks when the header X-Requested-With is present].
|
||||
See http://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html[this webappsec.org thread] for details on how to perform the exploit.
|
||||
See https://web.archive.org/web/20210221120355/https://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html[this webappsec.org thread] for details on how to perform the exploit.
|
||||
Another disadvantage is that by removing the state (that is, the timeout), you lose the ability to forcibly invalidate the token if it is compromised.
|
||||
|
||||
// FIXME: Document timeout with lengthy form expire. We do not want to automatically replay that request because it can lead to exploit
|
||||
|
||||
@@ -7,7 +7,7 @@ This portion of the documentation discusses the general topic of Security HTTP R
|
||||
Refer to the relevant sections for specific information on Security HTTP Response Headers <<servlet-headers,servlet>> and <<webflux-headers,WebFlux>> based applications.
|
||||
====
|
||||
|
||||
There are many https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Headers[HTTP response headers] that can be used to increase the security of web applications.
|
||||
There are many https://owasp.org/www-project-secure-headers/#div-headers[HTTP response headers] that can be used to increase the security of web applications.
|
||||
This section is dedicated to the various HTTP response headers that Spring Security provides explicit support for.
|
||||
If necessary, Spring Security can also be configured to provide <<headers-custom,custom headers>>.
|
||||
|
||||
@@ -267,7 +267,7 @@ Content-Security-Policy: script-src https://trustedscripts.example.com; report-u
|
||||
----
|
||||
====
|
||||
|
||||
https://www.w3.org/TR/CSP2/#violation-reports[Violation reports] are standard JSON structures that can be captured either by the web application's own API or by a publicly hosted CSP violation reporting service, such as, https://report-uri.io/.
|
||||
https://www.w3.org/TR/CSP2/#violation-reports[Violation reports] are standard JSON structures that can be captured either by the web application's own API or by a publicly hosted CSP violation reporting service, such as, https://report-uri.com/.
|
||||
|
||||
The `Content-Security-Policy-Report-Only` header provides the capability for web application authors and administrators to monitor security policies, rather than enforce them.
|
||||
This header is typically used when experimenting and/or developing security policies for a site.
|
||||
|
||||
@@ -25,7 +25,7 @@ Without proper configuration, the application server will not know that the load
|
||||
|
||||
To fix this you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
|
||||
To make the application aware of this, you need to either configure your application server aware of the X-Forwarded headers.
|
||||
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
|
||||
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
|
||||
Alternatively, Spring users can leverage https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java[ForwardedHeaderFilter].
|
||||
|
||||
Spring Boot users may use the `server.use-forward-headers` property to configure the application.
|
||||
|
||||
@@ -24,7 +24,7 @@ The topics in this section provide detail on how to consume Spring Security when
|
||||
=== Spring Boot with Maven
|
||||
|
||||
Spring Boot provides a `spring-boot-starter-security` starter that aggregates Spring Security-related dependencies together.
|
||||
The simplest and preferred way to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/htmlsingle/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
|
||||
The simplest and preferred way to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/html/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
|
||||
|
||||
Alternatively, you can manually add the starter, as the following example shows:
|
||||
|
||||
@@ -196,7 +196,7 @@ The following topics provide detail on how to consume Spring Security when using
|
||||
=== Spring Boot with Gradle
|
||||
|
||||
Spring Boot provides a `spring-boot-starter-security` starter that aggregates Spring Security related dependencies together.
|
||||
The simplest and preferred method to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/htmlsingle/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
|
||||
The simplest and preferred method to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/html/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
|
||||
|
||||
Alternatively, you can manually add the starter, as the following example shows:
|
||||
|
||||
|
||||
@@ -2,3 +2,9 @@
|
||||
= Samples
|
||||
|
||||
Spring Security includes many {gh-samples-url}[samples] applications.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
These samples are being migrated to a separate project, however, you can still find
|
||||
the not migrated samples in an older branch of the {gh-old-samples-url}[Spring Security repository].
|
||||
====
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
[[webflux-cors]]
|
||||
== CORS
|
||||
= CORS
|
||||
|
||||
Spring Framework provides https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-cors-intro[first class support for CORS].
|
||||
CORS must be processed before Spring Security because the pre-flight request will not contain any cookies (i.e. the `JSESSIONID`).
|
||||
|
||||
@@ -14,6 +14,8 @@ include::webclient.adoc[leveloffset=+1]
|
||||
|
||||
include::method.adoc[leveloffset=+1]
|
||||
|
||||
include::cors.adoc[leveloffset=+1]
|
||||
|
||||
include::test.adoc[leveloffset=+1]
|
||||
|
||||
include::rsocket.adoc[leveloffset=+1]
|
||||
|
||||
@@ -234,4 +234,4 @@ class SecurityConfig {
|
||||
----
|
||||
====
|
||||
|
||||
You can find a complete sample in {gh-samples-url}/javaconfig/hellowebflux-method[hellowebflux-method]
|
||||
You can find a complete sample in {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method]
|
||||
|
||||
@@ -12,7 +12,7 @@ NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as
|
||||
|
||||
Spring Boot 2.0 brings full auto-configuration capabilities for OAuth 2.0 Login.
|
||||
|
||||
This section shows how to configure the {gh-samples-url}/boot/oauth2login-webflux[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
|
||||
This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
|
||||
|
||||
* <<webflux-oauth2-login-sample-setup,Initial setup>>
|
||||
* <<webflux-oauth2-login-sample-redirect,Setting the redirect URI>>
|
||||
|
||||
@@ -11,7 +11,7 @@ This authorization server can be consulted by resource servers to authorize requ
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A complete working example for {gh-samples-url}/boot/oauth2resourceserver-webflux[*JWTs*] is available in the {gh-samples-url}[Spring Security repository].
|
||||
A complete working example for {gh-samples-url}/reactive/webflux/java/oauth2/resource-server[*JWTs*] is available in the {gh-samples-url}[Spring Security repository].
|
||||
====
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-minimaldependencies]]
|
||||
@@ -818,7 +818,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 30 seconds.
|
||||
By default, Resource Server configures a clock skew of 60 seconds.
|
||||
|
||||
[[webflux-oauth2resourceserver-validation-custom]]
|
||||
==== Configuring a Custom Validator
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ Spring Security allows resolving an access token using `@RegisteredOAuth2Authori
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A working example can be found in {gh-samples-url}/boot/oauth2webclient-webflux[*OAuth 2.0 WebClient WebFlux sample*].
|
||||
A working example can be found in {gh-samples-url}/reactive/webflux/java/oauth2/webclient[*OAuth 2.0 WebClient WebFlux sample*].
|
||||
====
|
||||
|
||||
After configuring Spring Security for <<webflux-oauth2-login,OAuth2 Login>> or as an <<webflux-oauth2-client,OAuth2 Client>>, an `OAuth2AuthorizedClient` can be resolved using the following:
|
||||
|
||||
@@ -6,7 +6,7 @@ The main entry point into security is found in the `PayloadSocketAcceptorInterce
|
||||
|
||||
You can find a few sample applications that demonstrate the code below:
|
||||
|
||||
* Hello RSocket {gh-samples-url}/boot/hellorsocket[hellorsocket]
|
||||
* Hello RSocket {gh-samples-url}/reactive/rsocket/hello-security[hellorsocket]
|
||||
* https://github.com/rwinch/spring-flights/tree/security[Spring Flights]
|
||||
|
||||
|
||||
@@ -43,9 +43,8 @@ In a Spring Boot application this is done automatically using `RSocketSecurityAu
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ServerRSocketFactoryProcessor springSecurityRSocketSecurity(
|
||||
SecuritySocketAcceptorInterceptor interceptor) {
|
||||
return builder -> builder.addSocketAcceptorPlugin(interceptor);
|
||||
RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
|
||||
return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor));
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
Spring Security's WebFlux support relies on a `WebFilter` and works the same for Spring WebFlux and Spring WebFlux.Fn.
|
||||
You can find a few sample applications that demonstrate the code below:
|
||||
|
||||
* Hello WebFlux {gh-samples-url}/boot/hellowebflux[hellowebflux]
|
||||
* Hello WebFlux.Fn {gh-samples-url}/boot/hellowebfluxfn[hellowebfluxfn]
|
||||
* Hello WebFlux Method {gh-samples-url}/boot/hellowebflux-method[hellowebflux-method]
|
||||
* Hello WebFlux {gh-samples-url}/reactive/webflux/java/hello-security[hellowebflux]
|
||||
* Hello WebFlux.Fn {gh-samples-url}/reactive/webflux-fn/hello-security[hellowebfluxfn]
|
||||
* Hello WebFlux Method {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method]
|
||||
|
||||
|
||||
== Minimal WebFlux Security Configuration
|
||||
|
||||
@@ -257,7 +257,7 @@ To get around this, you can disable session fixation protection, but in newer Se
|
||||
Note that switching between HTTP and HTTPS is not a good idea in general, as any application which uses HTTP at all is vulnerable to man-in-the-middle attacks.
|
||||
To be truly secure, the user should begin accessing your site in HTTPS and continue using it until they log out.
|
||||
Even clicking on an HTTPS link from a page accessed over HTTP is potentially risky.
|
||||
If you need more convincing, check out a tool like https://www.thoughtcrime.org/software/sslstrip/[sslstrip].
|
||||
If you need more convincing, check out a tool like https://github.com/moxie0/sslstrip/[sslstrip].
|
||||
|
||||
|
||||
==== I'm not switching between HTTP and HTTPS but my session is still getting lost
|
||||
@@ -378,7 +378,7 @@ For third-party jars the situation isn't always quite so obvious.
|
||||
A good starting point is to copy those from one of the pre-built sample applications WEB-INF/lib directories.
|
||||
For a basic application, you can start with the tutorial sample.
|
||||
If you want to use LDAP, with an embedded test server, then use the LDAP sample as a starting point.
|
||||
The reference manual also includes https://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#appendix-dependencies[an appendix] listing the first-level dependencies for each Spring Security module with some information on whether they are optional and what they are required for.
|
||||
The reference manual also includes {security-reference-url}#modules[an appendix] listing the first-level dependencies for each Spring Security module with some information on whether they are optional and what they are required for.
|
||||
|
||||
If you are building your project with maven, then adding the appropriate Spring Security modules as dependencies to your pom.xml will automatically pull in the core jars that the framework requires.
|
||||
Any which are marked as "optional" in the Spring Security POM files will have to be added to your own pom.xml file if you need them.
|
||||
|
||||
@@ -7,7 +7,7 @@ Without proper configuration, the application server will not know that the load
|
||||
|
||||
To fix this you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
|
||||
To make the application aware of this, you need to either configure your application server aware of the X-Forwarded headers.
|
||||
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
|
||||
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
|
||||
Alternatively, Spring 4.3+ users can leverage https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java[ForwardedHeaderFilter].
|
||||
|
||||
Spring Boot users may use the `server.use-forward-headers` property to configure the application.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[[servlet-delegatingfilterproxy]]
|
||||
= DelegatingFilterProxy
|
||||
|
||||
Spring provides a `Filter` implementation named {security-api-url}org/springframework/web/filter/DelegatingFilterProxy.html/[`DelegatingFilterProxy`] that allows bridging between the Servlet container's lifecycle and Spring's `ApplicationContext`.
|
||||
Spring provides a `Filter` implementation named {spring-framework-api-url}org/springframework/web/filter/DelegatingFilterProxy.html[`DelegatingFilterProxy`] that allows bridging between the Servlet container's lifecycle and Spring's `ApplicationContext`.
|
||||
The Servlet container allows registering ``Filter``s using its own standards, but it is not aware of Spring defined Beans.
|
||||
`DelegatingFilterProxy` can be registered via standard Servlet container mechanisms, but delegate all the work to a Spring Bean that implements `Filter`.
|
||||
|
||||
|
||||
@@ -100,3 +100,47 @@ This is an example of the use of the `AuthenticatedVoter` which we will see in t
|
||||
It uses an `AuthenticationTrustResolver` to process this particular configuration attribute and grant access to anonymous users.
|
||||
The `AuthenticatedVoter` approach is more powerful, since it allows you to differentiate between anonymous, remember-me and fully-authenticated users.
|
||||
If you don't need this functionality though, then you can stick with `ROLE_ANONYMOUS`, which will be processed by Spring Security's standard `RoleVoter`.
|
||||
|
||||
[[anonymous-auth-mvc-controller]]
|
||||
=== Getting Anonymous Authentications with Spring MVC
|
||||
|
||||
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments[Spring MVC resolves parameters of type `Principal`] using its own argument resolver.
|
||||
|
||||
This means that a construct like this one:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@GetMapping("/")
|
||||
public String method(Authentication authentication) {
|
||||
if (authentication instanceof AnonymousAuthenticationToken) {
|
||||
return "anonymous";
|
||||
} else {
|
||||
return "not anonymous";
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
will always return "not anonymous", even for anonymous requests.
|
||||
The reason is that Spring MVC resolves the parameter using `HttpServletRequest#getPrincipal`, which is `null` when the request is anonymous.
|
||||
|
||||
If you'd like to obtain the `Authentication` in anonymous requests, use `@CurrentSecurityContext` instead:
|
||||
|
||||
.Use CurrentSecurityContext for Anonymous requests
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
public String method(@CurrentSecurityContext SecurityContext context) {
|
||||
return context.getAuthentication().getName();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
fun method(@CurrentSecurityContext context : SecurityContext) : String =
|
||||
context!!.authentication!!.name
|
||||
----
|
||||
====
|
||||
|
||||
@@ -117,7 +117,7 @@ class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticatio
|
||||
|
||||
[[jaas-jaasauthenticationprovider]]
|
||||
=== JaasAuthenticationProvider
|
||||
The `JaasAuthenticationProvider` assumes the default `Configuration` is an instance of https://download.oracle.com/javase/1.4.2/docs/guide/security/jaas/spec/com/sun/security/auth/login/ConfigFile.html[ ConfigFile].
|
||||
The `JaasAuthenticationProvider` assumes the default `Configuration` is an instance of https://docs.oracle.com/javase/8/docs/jre/api/security/jaas/spec/com/sun/security/auth/login/ConfigFile.html[ ConfigFile].
|
||||
This assumption is made in order to attempt to update the `Configuration`.
|
||||
The `JaasAuthenticationProvider` then uses the default `Configuration` to create the `LoginContext`.
|
||||
|
||||
|
||||
@@ -147,5 +147,4 @@ It's also assumed that you have added a `UserDetailsService` (called "userDetail
|
||||
The class `J2eePreAuthenticatedProcessingFilter` will extract the username from the `userPrincipal` property of the `HttpServletRequest`.
|
||||
Use of this filter would usually be combined with the use of Java EE roles as described above in <<j2ee-preauth-details>>.
|
||||
|
||||
There is a sample application in the codebase which uses this approach, so get hold of the code from github and have a look at the application context file if you are interested.
|
||||
The code is in the `samples/xml/preauth` directory.
|
||||
There is a {gh-old-samples-url}/xml/preauth[sample application] in the samples project which uses this approach, so get hold of the code from GitHub and have a look at the application context file if you are interested.
|
||||
|
||||
@@ -51,7 +51,7 @@ If you have more than one in your application context, you need to specify which
|
||||
|
||||
[[remember-me-persistent-token]]
|
||||
=== Persistent Token Approach
|
||||
This approach is based on the article http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
There is a discussion on this in the comments section of this article.].
|
||||
To use the this approach with namespace configuration, you would supply a datasource reference:
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ This means that you can easily use X.509 authentication with other options such
|
||||
|
||||
[[x509-ssl-config]]
|
||||
=== Setting up SSL in Tomcat
|
||||
There are some pre-generated certificates in the `samples/certificate` directory in the Spring Security project.
|
||||
There are some pre-generated certificates in the {gh-samples-url}/servlet/java-configuration/authentication/x509/server[Spring Security Samples repository].
|
||||
You can use these to enable SSL for testing if you don't want to generate your own.
|
||||
The file `server.jks` contains the server certificate, private key and the issuing certificate authority certificate.
|
||||
There are also some client certificate files for the users from the sample applications.
|
||||
|
||||
@@ -375,7 +375,7 @@ For details, refer to the <<servlet-csrf-configure-custom-repository>> section.
|
||||
|
||||
If a token does expire, you might want to customize how it is handled by specifying a custom `AccessDeniedHandler`.
|
||||
The custom `AccessDeniedHandler` can process the `InvalidCsrfTokenException` any way you like.
|
||||
For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both <<nsa-access-denied-handler,xml>> and https://github.com/spring-projects/spring-security/blob/3.2.0.RC1/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAccessDeniedHandlerTests.java#L64[Java configuration].
|
||||
For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both <<nsa-access-denied-handler,xml>> and {gh-url}/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java#L64[Java configuration].
|
||||
// FIXME: We should add a custom AccessDeniedHandler section in the reference and update the links above
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ fun httpFirewall(): StrictHttpFirewall {
|
||||
----
|
||||
====
|
||||
|
||||
The `StrictHttpFirewall` provides an allowed list of valid HTTP methods that are allowed to protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering].
|
||||
The `StrictHttpFirewall` provides an allowed list of valid HTTP methods that are allowed to protect against https://owasp.org/www-community/attacks/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/06-Test_HTTP_Methods[HTTP Verb Tampering].
|
||||
The default valid methods are "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT".
|
||||
If your application needs to modify the valid methods, you can configure a custom `StrictHttpFirewall` bean.
|
||||
For example, the following will only allow HTTP "GET" and "POST" methods:
|
||||
|
||||
@@ -5,7 +5,7 @@ This section covers the minimum setup for how to use Spring Security with Spring
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The completed application can be found at {gh-samples-url}/boot/helloworld[samples/boot/helloworld]
|
||||
The completed application can be found {gh-samples-url}/servlet/spring-boot/java/hello-security[in our samples repository].
|
||||
For your convenience, you can download a minimal Spring Boot + Spring Security application by https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello%20Security&packageName=example.hello-security&dependencies=web,security[clicking here].
|
||||
====
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ This means developers need to explicitly protect their applications from externa
|
||||
==== Spring WebSocket Allowed Origin
|
||||
|
||||
Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins[current domain].
|
||||
Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defense_in_depth_%2528computing%2529[defence in depth].
|
||||
Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[defence in depth].
|
||||
|
||||
[[websocket-sameorigin-csrf]]
|
||||
==== Adding CSRF to Stomp Headers
|
||||
|
||||
@@ -20,7 +20,7 @@ This is much simpler than wiring up the equivalent Apache Directory Server beans
|
||||
The most common alternative configuration requirements are supported by attributes on the `ldap-server` element and the user is isolated from worrying about which beans they need to create and what the bean property names are.
|
||||
footnote:[You can find out more about the use of the `ldap-server` element in the chapter on pass:specialcharacters,macros[<<servlet-authentication-ldap>>].].
|
||||
Use of a good XML editor while editing the application context file should provide information on the attributes and elements that are available.
|
||||
We would recommend that you try out the https://spring.io/tools/sts[Spring Tool Suite] as it has special features for working with standard Spring namespaces.
|
||||
We would recommend that you try out the https://spring.io/tools[Eclipse IDE with Spring Tools] as it has special features for working with standard Spring namespaces.
|
||||
|
||||
|
||||
To start using the security namespace in your application context, you need to have the `spring-security-config` jar on your classpath.
|
||||
|
||||
@@ -12,7 +12,7 @@ NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as
|
||||
|
||||
Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login.
|
||||
|
||||
This section shows how to configure the {gh-samples-url}/boot/oauth2login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
|
||||
This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
|
||||
|
||||
* <<oauth2login-sample-initial-setup,Initial setup>>
|
||||
* <<oauth2login-sample-redirect-uri,Setting the redirect URI>>
|
||||
|
||||
@@ -15,7 +15,7 @@ This section provides details on how Spring Security provides support for OAuth
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Working samples for both {gh-samples-url}/boot/oauth2resourceserver[JWTs] and {gh-samples-url}/boot/oauth2resourceserver-opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security repository].
|
||||
Working samples for both {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/jwe[JWTs] and {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security Samples repository].
|
||||
====
|
||||
|
||||
Let's take a look at how Bearer Token Authentication works within Spring Security.
|
||||
@@ -31,7 +31,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to
|
||||
image:{icondir}/number_2.png[] Spring Security's <<servlet-authorization-filtersecurityinterceptor,`FilterSecurityInterceptor`>> indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
|
||||
|
||||
image:{icondir}/number_3.png[] Since the user is not authenticated, <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>> initiates __Start Authentication__.
|
||||
The configured <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header.
|
||||
The configured <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header.
|
||||
The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested.
|
||||
|
||||
When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token.
|
||||
@@ -1068,7 +1068,7 @@ fun jwtDecoder(): JwtDecoder {
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 30 seconds.
|
||||
By default, Resource Server configures a clock skew of 60 seconds.
|
||||
|
||||
[[oauth2resourceserver-jwt-validation-custom]]
|
||||
==== Configuring a Custom Validator
|
||||
|
||||
@@ -16,7 +16,7 @@ This process is similar to the one started in 2017 for <<oauth2,Spring Security'
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A working sample for {gh-samples-url}/boot/saml2login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security repository].
|
||||
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
|
||||
====
|
||||
|
||||
Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security.
|
||||
@@ -319,7 +319,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
||||
The above requires the role of `USER` for any URL that starts with `/messages/`.
|
||||
|
||||
[[servlet-saml2login-relyingpartyregistrationrepository]]
|
||||
The second `@Bean` Spring Boot creates is a {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository`], which represents the asserting party and relying party metadata.
|
||||
The second `@Bean` Spring Boot creates is a {old-security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository`], which represents the asserting party and relying party metadata.
|
||||
This includes things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
|
||||
|
||||
You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
|
||||
@@ -422,7 +422,7 @@ A relying party can be multi-tenant by registering more than one relying party i
|
||||
|
||||
[[servlet-saml2login-relyingpartyregistration]]
|
||||
=== RelyingPartyRegistration
|
||||
A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
|
||||
A {old-security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
|
||||
instance represents a link between an relying party and assering party's metadata.
|
||||
|
||||
In a `RelyingPartyRegistration`, you can provide relying party metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
= Spring Security Reference
|
||||
Ben Alex; Luke Taylor; Rob Winch; Gunnar Hillert; Joe Grandja; Jay Bryant; Eddú Meléndez; Josh Cummings; Dave Syer; Eleftheria Stein
|
||||
:include-dir: _includes
|
||||
:security-api-url: https://docs.spring.io/spring-security/site/docs/current/api/
|
||||
:source-indent: 0
|
||||
:tabsize: 4
|
||||
:toc: left
|
||||
|
||||
+3
-2
@@ -2,8 +2,9 @@ aspectjVersion=1.9.6
|
||||
springJavaformatVersion=0.0.28
|
||||
springBootVersion=2.4.2
|
||||
openSamlVersion=3.4.6
|
||||
version=5.5.0
|
||||
kotlinVersion=1.5.0
|
||||
version=5.5.1
|
||||
kotlinVersion=1.5.10
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
+25
-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.
|
||||
@@ -54,7 +54,17 @@ public class DefaultSpringSecurityContextSourceTests {
|
||||
|
||||
@Test
|
||||
public void supportsSpacesInUrl() {
|
||||
new DefaultSpringSecurityContextSource("ldap://myhost:10389/dc=spring%20framework,dc=org");
|
||||
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
|
||||
"ldap://myhost:10389/dc=spring%20framework,dc=org");
|
||||
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("dc=spring framework,dc=org");
|
||||
}
|
||||
|
||||
// gh-9742
|
||||
@Test
|
||||
public void constructorWhenUrlEncodedSpacesWithPlusCharacterThenBaseDnIsProperlyDecoded() {
|
||||
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
|
||||
"ldap://blah:123/dc=spring+framework,dc=org ldap://blah:456/dc=spring+framework,dc=org");
|
||||
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("dc=spring framework,dc=org");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,6 +104,7 @@ public class DefaultSpringSecurityContextSourceTests {
|
||||
public void serverUrlWithSpacesIsSupported() {
|
||||
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
|
||||
this.contextSource.getUrls()[0] + "ou=space%20cadets,dc=springframework,dc=org");
|
||||
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("ou=space cadets,dc=springframework,dc=org");
|
||||
contextSource.afterPropertiesSet();
|
||||
contextSource.getContext("uid=space cadet,ou=space cadets,dc=springframework,dc=org", "spacecadetspassword");
|
||||
}
|
||||
@@ -135,6 +146,18 @@ public class DefaultSpringSecurityContextSourceTests {
|
||||
assertThat(ctxSrc.isPooled()).isTrue();
|
||||
}
|
||||
|
||||
// gh-9742
|
||||
@Test
|
||||
public void constructorWhenServerListWithSpacesInBaseDnThenSuccess() {
|
||||
List<String> serverUrls = new ArrayList<>();
|
||||
serverUrls.add("ldap://ad1.example.org:789");
|
||||
serverUrls.add("ldap://ad2.example.org:389");
|
||||
serverUrls.add("ldaps://ad3.example.org:636");
|
||||
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(serverUrls,
|
||||
"dc=spring framework,dc=org");
|
||||
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("dc=spring framework,dc=org");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void instantiationFailsWithIncorrectServerUrl() {
|
||||
List<String> serverUrls = new ArrayList<>();
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.ldap.search;
|
||||
|
||||
import javax.naming.ldap.LdapName;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.ldap.core.ContextSource;
|
||||
import org.springframework.ldap.core.DirContextOperations;
|
||||
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
|
||||
import org.springframework.security.ldap.server.ApacheDSContainer;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Additional tests for {@link FilterBasedLdapUserSearch} with spaces in the base dn.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = FilterBasedLdapUserSearchWithSpacesTests.ApacheDsContainerWithSpacesConfig.class)
|
||||
public class FilterBasedLdapUserSearchWithSpacesTests {
|
||||
|
||||
@Autowired
|
||||
private DefaultSpringSecurityContextSource contextSource;
|
||||
|
||||
// gh-9742
|
||||
@Test
|
||||
public void searchForUserWhenSpacesInBaseDnThenSuccess() throws Exception {
|
||||
FilterBasedLdapUserSearch locator = new FilterBasedLdapUserSearch("ou=space cadets", "(uid={0})",
|
||||
this.contextSource);
|
||||
locator.setSearchSubtree(false);
|
||||
locator.setSearchTimeLimit(0);
|
||||
locator.setDerefLinkFlag(false);
|
||||
|
||||
DirContextOperations bob = locator.searchForUser("space cadet");
|
||||
assertThat(bob.getStringAttribute("uid")).isEqualTo("space cadet");
|
||||
assertThat(bob.getDn()).isEqualTo(new LdapName("uid=space cadet,ou=space cadets"));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ApacheDsContainerWithSpacesConfig implements DisposableBean {
|
||||
|
||||
private ApacheDSContainer container;
|
||||
|
||||
@Bean
|
||||
ApacheDSContainer ldapContainer() throws Exception {
|
||||
this.container = new ApacheDSContainer("dc=spring framework,dc=org",
|
||||
"classpath:test-server-with-spaces.ldif");
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ContextSource contextSource(ApacheDSContainer ldapContainer) {
|
||||
return new DefaultSpringSecurityContextSource(
|
||||
"ldap://127.0.0.1:" + ldapContainer.getLocalPort() + "/dc=spring%20framework,dc=org");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
this.container.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
dn: ou=space cadets,dc=spring framework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: space cadets
|
||||
|
||||
dn: uid=space cadet,ou=space cadets,dc=spring framework,dc=org
|
||||
objectclass: top
|
||||
objectclass: person
|
||||
objectclass: organizationalPerson
|
||||
objectclass: inetOrgPerson
|
||||
cn: Space Cadet
|
||||
sn: Cadet
|
||||
uid: space cadet
|
||||
userPassword: spacecadetspassword
|
||||
+26
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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,10 @@
|
||||
|
||||
package org.springframework.security.ldap;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
@@ -74,7 +78,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
|
||||
rootDn = (rootDn != null) ? rootDn : urlRootDn;
|
||||
}
|
||||
setUrls(urls.toArray(new String[0]));
|
||||
setBase(rootDn);
|
||||
setBase((rootDn != null) ? decodeUrl(rootDn) : null);
|
||||
setPooled(true);
|
||||
setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy() {
|
||||
|
||||
@@ -136,7 +140,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
|
||||
private static String buildProviderUrl(List<String> urls, String baseDn) {
|
||||
Assert.notNull(baseDn, "The Base DN for the LDAP server must not be null.");
|
||||
Assert.notEmpty(urls, "At least one LDAP server URL must be provided.");
|
||||
String trimmedBaseDn = baseDn.trim();
|
||||
String encodedBaseDn = encodeUrl(baseDn.trim());
|
||||
StringBuilder providerUrl = new StringBuilder();
|
||||
for (String serverUrl : urls) {
|
||||
String trimmedUrl = serverUrl.trim();
|
||||
@@ -147,11 +151,29 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
|
||||
if (!trimmedUrl.endsWith("/")) {
|
||||
providerUrl.append("/");
|
||||
}
|
||||
providerUrl.append(trimmedBaseDn);
|
||||
providerUrl.append(encodedBaseDn);
|
||||
providerUrl.append(" ");
|
||||
}
|
||||
return providerUrl.toString();
|
||||
|
||||
}
|
||||
|
||||
private static String encodeUrl(String url) {
|
||||
try {
|
||||
return URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String decodeUrl(String url) {
|
||||
try {
|
||||
return URLDecoder.decode(url, StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-2
@@ -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,9 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -97,7 +100,19 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
|
||||
|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
String clientId = encodeClientCredential(clientRegistration.getClientId());
|
||||
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
|
||||
headers.setBasicAuth(clientId, clientSecret);
|
||||
}
|
||||
}
|
||||
|
||||
private static String encodeClientCredential(String clientCredential) {
|
||||
try {
|
||||
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
// Will not happen since UTF-8 is a standard charset
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-4
@@ -80,7 +80,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
||||
|
||||
private final Function<ClientRegistration, JWK> jwkResolver;
|
||||
|
||||
private final Map<String, NimbusJwsEncoder> jwsEncoders = new ConcurrentHashMap<>();
|
||||
private final Map<String, JwsEncoderHolder> jwsEncoders = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a {@code NimbusJwtClientAuthenticationParametersConverter} using the
|
||||
@@ -140,12 +140,16 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
||||
JoseHeader joseHeader = headersBuilder.build();
|
||||
JwtClaimsSet jwtClaimsSet = claimsBuilder.build();
|
||||
|
||||
NimbusJwsEncoder jwsEncoder = this.jwsEncoders.computeIfAbsent(clientRegistration.getRegistrationId(),
|
||||
(clientRegistrationId) -> {
|
||||
JwsEncoderHolder jwsEncoderHolder = this.jwsEncoders.compute(clientRegistration.getRegistrationId(),
|
||||
(clientRegistrationId, currentJwsEncoderHolder) -> {
|
||||
if (currentJwsEncoderHolder != null && currentJwsEncoderHolder.getJwk().equals(jwk)) {
|
||||
return currentJwsEncoderHolder;
|
||||
}
|
||||
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
||||
return new NimbusJwsEncoder(jwkSource);
|
||||
return new JwsEncoderHolder(new NimbusJwsEncoder(jwkSource), jwk);
|
||||
});
|
||||
|
||||
NimbusJwsEncoder jwsEncoder = jwsEncoderHolder.getJwsEncoder();
|
||||
Jwt jws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
@@ -180,4 +184,25 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
||||
return jwsAlgorithm;
|
||||
}
|
||||
|
||||
private static final class JwsEncoderHolder {
|
||||
|
||||
private final NimbusJwsEncoder jwsEncoder;
|
||||
|
||||
private final JWK jwk;
|
||||
|
||||
private JwsEncoderHolder(NimbusJwsEncoder jwsEncoder, JWK jwk) {
|
||||
this.jwsEncoder = jwsEncoder;
|
||||
this.jwk = jwk;
|
||||
}
|
||||
|
||||
private NimbusJwsEncoder getJwsEncoder() {
|
||||
return this.jwsEncoder;
|
||||
}
|
||||
|
||||
private JWK getJwk() {
|
||||
return this.jwk;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-2
@@ -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,9 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
@@ -48,11 +51,23 @@ final class OAuth2AuthorizationGrantRequestEntityUtils {
|
||||
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
|
||||
|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
String clientId = encodeClientCredential(clientRegistration.getClientId());
|
||||
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
|
||||
headers.setBasicAuth(clientId, clientSecret);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static String encodeClientCredential(String clientCredential) {
|
||||
try {
|
||||
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
// Will not happen since UTF-8 is a standard charset
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpHeaders getDefaultTokenRequestHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler {
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
if (!HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
|
||||
if (HttpStatus.BAD_REQUEST.value() != response.getRawStatusCode()) {
|
||||
this.defaultErrorHandler.handleError(response);
|
||||
}
|
||||
// A Bearer Token Error may be in the WWW-Authenticate response header
|
||||
|
||||
+4
-4
@@ -67,7 +67,7 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
|
||||
URI endSessionEndpoint = this.endSessionEndpoint(clientRegistration);
|
||||
if (endSessionEndpoint != null) {
|
||||
String idToken = idToken(authentication);
|
||||
URI postLogoutRedirectUri = postLogoutRedirectUri(request);
|
||||
String postLogoutRedirectUri = postLogoutRedirectUri(request);
|
||||
targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
|
||||
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
|
||||
}
|
||||
|
||||
private URI postLogoutRedirectUri(HttpServletRequest request) {
|
||||
private String postLogoutRedirectUri(HttpServletRequest request) {
|
||||
if (this.postLogoutRedirectUri == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -102,11 +102,11 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
|
||||
.build();
|
||||
return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri)
|
||||
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
|
||||
.toUri();
|
||||
.toUriString();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private String endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) {
|
||||
private String endpointUri(URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
|
||||
builder.queryParam("id_token_hint", idToken);
|
||||
if (postLogoutRedirectUri != null) {
|
||||
|
||||
+5
-17
@@ -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.
|
||||
@@ -23,7 +23,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.GrantType;
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||
@@ -240,15 +239,7 @@ public final class ClientRegistrations {
|
||||
() -> "The Issuer \"" + metadataIssuer + "\" provided in the configuration metadata did "
|
||||
+ "not match the requested issuer \"" + issuer + "\"");
|
||||
String name = URI.create(issuer).getHost();
|
||||
ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer,
|
||||
metadata.getTokenEndpointAuthMethods());
|
||||
List<GrantType> grantTypes = metadata.getGrantTypes();
|
||||
// If null, the default includes authorization_code
|
||||
if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer
|
||||
+ "\" returned a configuration of " + grantTypes);
|
||||
}
|
||||
ClientAuthenticationMethod method = getClientAuthenticationMethod(metadata.getTokenEndpointAuthMethods());
|
||||
Map<String, Object> configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject());
|
||||
// @formatter:off
|
||||
return ClientRegistration.withRegistrationId(name)
|
||||
@@ -256,7 +247,7 @@ public final class ClientRegistrations {
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.clientAuthenticationMethod(method)
|
||||
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
|
||||
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
|
||||
.authorizationUri((metadata.getAuthorizationEndpointURI() != null) ? metadata.getAuthorizationEndpointURI().toASCIIString() : null)
|
||||
.providerConfigurationMetadata(configurationMetadata)
|
||||
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
|
||||
.issuerUri(issuer)
|
||||
@@ -264,7 +255,7 @@ public final class ClientRegistrations {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer,
|
||||
private static ClientAuthenticationMethod getClientAuthenticationMethod(
|
||||
List<com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod> metadataAuthMethods) {
|
||||
if (metadataAuthMethods == null || metadataAuthMethods
|
||||
.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
|
||||
@@ -277,10 +268,7 @@ public final class ClientRegistrations {
|
||||
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) {
|
||||
return ClientAuthenticationMethod.NONE;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Only ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST and "
|
||||
+ "ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer
|
||||
+ "\" returned a configuration of " + metadataAuthMethods);
|
||||
return null;
|
||||
}
|
||||
|
||||
private interface ThrowingFunction<S, T, E extends Throwable> {
|
||||
|
||||
+2
-2
@@ -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.
|
||||
@@ -221,7 +221,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
|
||||
|
||||
private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
|
||||
Exception ex) throws IOException {
|
||||
this.logger.error(LogMessage.format("Authorization Request failed: %s", ex, ex));
|
||||
this.logger.error(LogMessage.format("Authorization Request failed: %s", ex), ex);
|
||||
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
|
||||
}
|
||||
|
||||
+62
-51
@@ -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.
|
||||
@@ -34,6 +34,7 @@ import org.springframework.web.server.WebSession;
|
||||
* {@link OAuth2AuthorizationRequest} in the {@code WebSession}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Steve Riesenberg
|
||||
* @since 5.1
|
||||
* @see AuthorizationRequestRepository
|
||||
* @see OAuth2AuthorizationRequest
|
||||
@@ -46,6 +47,8 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
|
||||
|
||||
private final String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;
|
||||
|
||||
private boolean allowMultipleAuthorizationRequests;
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthorizationRequest> loadAuthorizationRequest(ServerWebExchange exchange) {
|
||||
String state = getStateParameter(exchange);
|
||||
@@ -53,7 +56,9 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
|
||||
return Mono.empty();
|
||||
}
|
||||
// @formatter:off
|
||||
return getStateToAuthorizationRequest(exchange)
|
||||
return this.getSessionAttributes(exchange)
|
||||
.filter((sessionAttrs) -> sessionAttrs.containsKey(this.sessionAttributeName))
|
||||
.map(this::getAuthorizationRequests)
|
||||
.filter((stateToAuthorizationRequest) -> stateToAuthorizationRequest.containsKey(state))
|
||||
.map((stateToAuthorizationRequest) -> stateToAuthorizationRequest.get(state));
|
||||
// @formatter:on
|
||||
@@ -63,10 +68,20 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
|
||||
public Mono<Void> saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
|
||||
ServerWebExchange exchange) {
|
||||
Assert.notNull(authorizationRequest, "authorizationRequest cannot be null");
|
||||
Assert.notNull(exchange, "exchange cannot be null");
|
||||
// @formatter:off
|
||||
return saveStateToAuthorizationRequest(exchange)
|
||||
.doOnNext((stateToAuthorizationRequest) -> stateToAuthorizationRequest
|
||||
.put(authorizationRequest.getState(), authorizationRequest))
|
||||
return getSessionAttributes(exchange)
|
||||
.doOnNext((sessionAttrs) -> {
|
||||
if (this.allowMultipleAuthorizationRequests) {
|
||||
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(
|
||||
sessionAttrs);
|
||||
authorizationRequests.put(authorizationRequest.getState(), authorizationRequest);
|
||||
sessionAttrs.put(this.sessionAttributeName, authorizationRequests);
|
||||
}
|
||||
else {
|
||||
sessionAttrs.put(this.sessionAttributeName, authorizationRequest);
|
||||
}
|
||||
})
|
||||
.then();
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -78,30 +93,21 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
|
||||
return Mono.empty();
|
||||
}
|
||||
// @formatter:off
|
||||
return exchange.getSession()
|
||||
.map(WebSession::getAttributes)
|
||||
.handle((sessionAttrs, sink) -> {
|
||||
Map<String, OAuth2AuthorizationRequest> stateToAuthzRequest = sessionAttrsMapStateToAuthorizationRequest(
|
||||
return getSessionAttributes(exchange)
|
||||
.flatMap((sessionAttrs) -> {
|
||||
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(
|
||||
sessionAttrs);
|
||||
if (stateToAuthzRequest == null) {
|
||||
sink.complete();
|
||||
return;
|
||||
}
|
||||
OAuth2AuthorizationRequest removedValue = stateToAuthzRequest.remove(state);
|
||||
if (stateToAuthzRequest.isEmpty()) {
|
||||
OAuth2AuthorizationRequest originalRequest = authorizationRequests.remove(state);
|
||||
if (authorizationRequests.isEmpty()) {
|
||||
sessionAttrs.remove(this.sessionAttributeName);
|
||||
}
|
||||
else if (removedValue != null) {
|
||||
// gh-7327 Overwrite the existing Map to ensure the state is saved for
|
||||
// distributed sessions
|
||||
sessionAttrs.put(this.sessionAttributeName, stateToAuthzRequest);
|
||||
}
|
||||
if (removedValue == null) {
|
||||
sink.complete();
|
||||
else if (authorizationRequests.size() == 1) {
|
||||
sessionAttrs.put(this.sessionAttributeName, authorizationRequests.values().iterator().next());
|
||||
}
|
||||
else {
|
||||
sink.next(removedValue);
|
||||
sessionAttrs.put(this.sessionAttributeName, authorizationRequests);
|
||||
}
|
||||
return Mono.justOrEmpty(originalRequest);
|
||||
});
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -120,36 +126,41 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
|
||||
return exchange.getSession().map(WebSession::getAttributes);
|
||||
}
|
||||
|
||||
private Mono<Map<String, OAuth2AuthorizationRequest>> getStateToAuthorizationRequest(ServerWebExchange exchange) {
|
||||
Assert.notNull(exchange, "exchange cannot be null");
|
||||
|
||||
// @formatter:off
|
||||
return getSessionAttributes(exchange)
|
||||
.flatMap((sessionAttrs) -> Mono.justOrEmpty(this.sessionAttrsMapStateToAuthorizationRequest(sessionAttrs)));
|
||||
// @formatter:on
|
||||
private Map<String, OAuth2AuthorizationRequest> getAuthorizationRequests(Map<String, Object> sessionAttrs) {
|
||||
Object sessionAttributeValue = sessionAttrs.get(this.sessionAttributeName);
|
||||
if (sessionAttributeValue == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
else if (sessionAttributeValue instanceof OAuth2AuthorizationRequest) {
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest = (OAuth2AuthorizationRequest) sessionAttributeValue;
|
||||
Map<String, OAuth2AuthorizationRequest> authorizationRequests = new HashMap<>(1);
|
||||
authorizationRequests.put(oauth2AuthorizationRequest.getState(), oauth2AuthorizationRequest);
|
||||
return authorizationRequests;
|
||||
}
|
||||
else if (sessionAttributeValue instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, OAuth2AuthorizationRequest> authorizationRequests = (Map<String, OAuth2AuthorizationRequest>) sessionAttrs
|
||||
.get(this.sessionAttributeName);
|
||||
return authorizationRequests;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(
|
||||
"authorizationRequests is supposed to be a Map or OAuth2AuthorizationRequest but actually is a "
|
||||
+ sessionAttributeValue.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Map<String, OAuth2AuthorizationRequest>> saveStateToAuthorizationRequest(ServerWebExchange exchange) {
|
||||
Assert.notNull(exchange, "exchange cannot be null");
|
||||
// @formatter:off
|
||||
return getSessionAttributes(exchange)
|
||||
.doOnNext((sessionAttrs) -> {
|
||||
Object stateToAuthzRequest = sessionAttrs.get(this.sessionAttributeName);
|
||||
if (stateToAuthzRequest == null) {
|
||||
stateToAuthzRequest = new HashMap<String, OAuth2AuthorizationRequest>();
|
||||
}
|
||||
// No matter stateToAuthzRequest was in session or not, we should always put
|
||||
// it into session again
|
||||
// in case of redis or hazelcast session. #6215
|
||||
sessionAttrs.put(this.sessionAttributeName, stateToAuthzRequest);
|
||||
})
|
||||
.flatMap((sessionAttrs) -> Mono.justOrEmpty(this.sessionAttrsMapStateToAuthorizationRequest(sessionAttrs)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Map<String, OAuth2AuthorizationRequest> sessionAttrsMapStateToAuthorizationRequest(
|
||||
Map<String, Object> sessionAttrs) {
|
||||
return (Map<String, OAuth2AuthorizationRequest>) sessionAttrs.get(this.sessionAttributeName);
|
||||
/**
|
||||
* Configure if multiple {@link OAuth2AuthorizationRequest}s should be stored per
|
||||
* session. Default is false (not allow multiple {@link OAuth2AuthorizationRequest}
|
||||
* per session).
|
||||
* @param allowMultipleAuthorizationRequests true allows more than one
|
||||
* {@link OAuth2AuthorizationRequest} to be stored per session.
|
||||
* @since 5.5
|
||||
*/
|
||||
@Deprecated
|
||||
public void setAllowMultipleAuthorizationRequests(boolean allowMultipleAuthorizationRequests) {
|
||||
this.allowMultipleAuthorizationRequests = allowMultipleAuthorizationRequests;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+56
@@ -16,7 +16,12 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
@@ -42,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -172,4 +178,54 @@ public class NimbusJwtClientAuthenticationParametersConverterTests {
|
||||
assertThat(jws.getExpiresAt()).isNotNull();
|
||||
}
|
||||
|
||||
// gh-9814
|
||||
@Test
|
||||
public void convertWhenClientKeyChangesThenNewKeyUsed() throws Exception {
|
||||
// @formatter:off
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
RSAKey rsaJwk1 = TestJwks.DEFAULT_RSA_JWK;
|
||||
given(this.jwkResolver.apply(eq(clientRegistration))).willReturn(rsaJwk1);
|
||||
|
||||
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
|
||||
clientRegistration);
|
||||
MultiValueMap<String, String> parameters = this.converter.convert(clientCredentialsGrantRequest);
|
||||
|
||||
String encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk1.toRSAPublicKey()).build();
|
||||
jwtDecoder.decode(encodedJws);
|
||||
|
||||
RSAKey rsaJwk2 = generateRsaJwk();
|
||||
given(this.jwkResolver.apply(eq(clientRegistration))).willReturn(rsaJwk2);
|
||||
|
||||
parameters = this.converter.convert(clientCredentialsGrantRequest);
|
||||
|
||||
encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
|
||||
jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk2.toRSAPublicKey()).build();
|
||||
jwtDecoder.decode(encodedJws);
|
||||
}
|
||||
|
||||
private static RSAKey generateRsaJwk() {
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
// @formatter:off
|
||||
return new RSAKey.Builder(publicKey)
|
||||
.privateKey(privateKey)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+38
@@ -16,6 +16,11 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
@@ -128,4 +133,37 @@ public class OAuth2ClientCredentialsGrantRequestEntityConverterTests {
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
|
||||
}
|
||||
|
||||
// gh-9610
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void convertWhenSpecialCharactersThenConvertsWithEncodedClientCredentials()
|
||||
throws UnsupportedEncodingException {
|
||||
String clientCredentialWithAnsiKeyboardSpecialCharacters = "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ ";
|
||||
// @formatter:off
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
|
||||
.clientId(clientCredentialWithAnsiKeyboardSpecialCharacters)
|
||||
.clientSecret(clientCredentialWithAnsiKeyboardSpecialCharacters)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
|
||||
clientRegistration);
|
||||
RequestEntity<?> requestEntity = this.converter.convert(clientCredentialsGrantRequest);
|
||||
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(requestEntity.getUrl().toASCIIString())
|
||||
.isEqualTo(clientRegistration.getProviderDetails().getTokenUri());
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getContentType())
|
||||
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
|
||||
String urlEncodedClientCredential = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters,
|
||||
StandardCharsets.UTF_8.toString());
|
||||
String clientCredentials = Base64.getEncoder().encodeToString(
|
||||
(urlEncodedClientCredential + ":" + urlEncodedClientCredential).getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic " + clientCredentials);
|
||||
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
|
||||
.isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue());
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+34
-1
@@ -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,10 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
@@ -89,6 +93,35 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests {
|
||||
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
|
||||
}
|
||||
|
||||
// gh-9610
|
||||
@Test
|
||||
public void getTokenResponseWhenSpecialCharactersThenSuccessWithEncodedClientCredentials() throws Exception {
|
||||
// @formatter:off
|
||||
enqueueJson("{\n"
|
||||
+ " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
|
||||
+ " \"token_type\":\"bearer\",\n"
|
||||
+ " \"expires_in\":3600,\n"
|
||||
+ " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n"
|
||||
+ " \"scope\":\"create\"\n"
|
||||
+ "}");
|
||||
// @formatter:on
|
||||
String clientCredentialWithAnsiKeyboardSpecialCharacters = "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ ";
|
||||
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(
|
||||
this.clientRegistration.clientId(clientCredentialWithAnsiKeyboardSpecialCharacters)
|
||||
.clientSecret(clientCredentialWithAnsiKeyboardSpecialCharacters).build());
|
||||
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
|
||||
RecordedRequest actualRequest = this.server.takeRequest();
|
||||
String body = actualRequest.getBody().readUtf8();
|
||||
assertThat(response.getAccessToken()).isNotNull();
|
||||
String urlEncodedClientCredentialecret = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters,
|
||||
StandardCharsets.UTF_8.toString());
|
||||
String clientCredentials = Base64.getEncoder()
|
||||
.encodeToString((urlEncodedClientCredentialecret + ":" + urlEncodedClientCredentialecret)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic " + clientCredentials);
|
||||
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTokenResponseWhenPostThenSuccess() throws Exception {
|
||||
ClientRegistration registration = this.clientRegistration
|
||||
|
||||
+49
@@ -16,12 +16,17 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.http;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.mock.http.MockHttpInputMessage;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.web.client.UnknownHttpStatusCodeException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
@@ -67,4 +72,48 @@ public class OAuth2ErrorResponseErrorHandlerTests {
|
||||
.isThrownBy(() -> this.errorHandler.handleError(response)).withMessage("[server_error] ");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleErrorWhenErrorResponseWithInvalidStatusCodeThenHandled() {
|
||||
CustomMockClientHttpResponse response = new CustomMockClientHttpResponse(new byte[0], 596);
|
||||
assertThatExceptionOfType(UnknownHttpStatusCodeException.class)
|
||||
.isThrownBy(() -> this.errorHandler.handleError(response)).withMessage("596 : [no body]");
|
||||
}
|
||||
|
||||
private static final class CustomMockClientHttpResponse extends MockHttpInputMessage implements ClientHttpResponse {
|
||||
|
||||
private final int statusCode;
|
||||
|
||||
private CustomMockClientHttpResponse(byte[] content, int statusCode) {
|
||||
super(content);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatus getStatusCode() throws IOException {
|
||||
return HttpStatus.valueOf(getRawStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRawStatusCode() {
|
||||
return this.statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatusText() throws IOException {
|
||||
HttpStatus httpStatus = HttpStatus.resolve(this.statusCode);
|
||||
return (httpStatus != null) ? httpStatus.getReasonPhrase() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
getBody().close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -138,6 +138,19 @@ public class OidcClientInitiatedLogoutSuccessHandlerTests {
|
||||
"https://endpoint?" + "id_token_hint=id-token&" + "post_logout_redirect_uri=https://rp.example.org");
|
||||
}
|
||||
|
||||
// gh-9511
|
||||
@Test
|
||||
public void logoutWhenUsingPostLogoutRedirectUriWithQueryParametersThenBuildsItForRedirect()
|
||||
throws IOException, ServletException {
|
||||
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(TestOidcUsers.create(),
|
||||
AuthorityUtils.NO_AUTHORITIES, this.registration.getRegistrationId());
|
||||
this.handler.setPostLogoutRedirectUri("https://rp.example.org/context?forwardUrl=secured%3Fparam%3Dtrue");
|
||||
this.request.setUserPrincipal(token);
|
||||
this.handler.onLogoutSuccess(this.request, this.response, token);
|
||||
assertThat(this.response.getRedirectedUrl()).isEqualTo("https://endpoint?id_token_hint=id-token&"
|
||||
+ "post_logout_redirect_uri=https://rp.example.org/context?forwardUrl%3Dsecured%253Fparam%253Dtrue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null));
|
||||
|
||||
+106
-34
@@ -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.
|
||||
@@ -240,24 +240,46 @@ public class ClientRegistrationsTests {
|
||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently only support authorization_code, so verify we have a meaningful error
|
||||
* until we add support.
|
||||
*/
|
||||
// gh-9828
|
||||
@Test
|
||||
public void issuerWhenGrantTypesSupportedInvalidThenException() {
|
||||
public void issuerWhenImplicitGrantTypeThenSuccess() throws Exception {
|
||||
this.response.put("grant_types_supported", Arrays.asList("implicit"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> registration(""))
|
||||
.withMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \""
|
||||
+ this.issuer + "\" returned a configuration of [implicit]");
|
||||
ClientRegistration registration = registration("").build();
|
||||
// The authorization_code grant type is still the default
|
||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
// gh-9828
|
||||
@Test
|
||||
public void issuerWhenOAuth2GrantTypesSupportedInvalidThenException() {
|
||||
this.response.put("grant_types_supported", Arrays.asList("implicit"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> registrationOAuth2("", null))
|
||||
.withMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \""
|
||||
+ this.issuer + "\" returned a configuration of [implicit]");
|
||||
public void issuerWhenOAuth2JwtBearerGrantTypeThenSuccess() throws Exception {
|
||||
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
|
||||
ClientRegistration registration = registrationOAuth2("", null).build();
|
||||
// The authorization_code grant type is still the default
|
||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
// gh-9795
|
||||
@Test
|
||||
public void issuerWhenResponseAuthorizationEndpointIsNullThenSuccess() throws Exception {
|
||||
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
|
||||
this.response.remove("authorization_endpoint");
|
||||
ClientRegistration registration = registration("").authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build();
|
||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
|
||||
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
|
||||
assertThat(provider.getAuthorizationUri()).isNull();
|
||||
}
|
||||
|
||||
// gh-9795
|
||||
@Test
|
||||
public void issuerWhenOAuth2ResponseAuthorizationEndpointIsNullThenSuccess() throws Exception {
|
||||
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
|
||||
this.response.remove("authorization_endpoint");
|
||||
ClientRegistration registration = registrationOAuth2("", null)
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER).build();
|
||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
|
||||
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
|
||||
assertThat(provider.getAuthorizationUri()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -276,6 +298,24 @@ public class ClientRegistrationsTests {
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenClientSecretBasicAuthMethodThenMethodIsBasic() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_basic"));
|
||||
ClientRegistration registration = registration("").build();
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenOAuth2ClientSecretBasicAuthMethodThenMethodIsBasic() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_basic"));
|
||||
ClientRegistration registration = registrationOAuth2("", null).build();
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
|
||||
@@ -292,6 +332,46 @@ public class ClientRegistrationsTests {
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_POST);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenClientSecretJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_jwt"));
|
||||
ClientRegistration registration = registration("").build();
|
||||
// The client_secret_basic auth method is still the default
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenOAuth2ClientSecretJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_jwt"));
|
||||
ClientRegistration registration = registrationOAuth2("", null).build();
|
||||
// The client_secret_basic auth method is still the default
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenPrivateKeyJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("private_key_jwt"));
|
||||
ClientRegistration registration = registration("").build();
|
||||
// The client_secret_basic auth method is still the default
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenOAuth2PrivateKeyJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("private_key_jwt"));
|
||||
ClientRegistration registration = registrationOAuth2("", null).build();
|
||||
// The client_secret_basic auth method is still the default
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void issuerWhenTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none"));
|
||||
@@ -306,32 +386,24 @@ public class ClientRegistrationsTests {
|
||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently only support client_secret_basic, so verify we have a meaningful error
|
||||
* until we add support.
|
||||
*/
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() {
|
||||
public void issuerWhenTlsClientAuthMethodThenSuccess() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth"));
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> registration(""))
|
||||
.withMessageContaining("Only ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST and "
|
||||
+ "ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer
|
||||
+ "\" returned a configuration of [tls_client_auth]");
|
||||
// @formatter:on
|
||||
ClientRegistration registration = registration("").build();
|
||||
// The client_secret_basic auth method is still the default
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
// gh-9780
|
||||
@Test
|
||||
public void issuerWhenOAuth2TokenEndpointAuthMethodsInvalidThenException() {
|
||||
public void issuerWhenOAuth2TlsClientAuthMethodThenSuccess() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth"));
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> registrationOAuth2("", null))
|
||||
.withMessageContaining("Only ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST and "
|
||||
+ "ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer
|
||||
+ "\" returned a configuration of [tls_client_auth]");
|
||||
// @formatter:on
|
||||
ClientRegistration registration = registrationOAuth2("", null).build();
|
||||
// The client_secret_basic auth method is still the default
|
||||
assertThat(registration.getClientAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebSessionOAuth2ServerAuthorizationRequestRepository} when
|
||||
* {@link WebSessionOAuth2ServerAuthorizationRequestRepository#setAllowMultipleAuthorizationRequests(boolean)}
|
||||
* is enabled.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
|
||||
public class WebSessionOAuth2ServerAuthorizationRequestRepositoryAllowMultipleAuthorizationRequestsTests
|
||||
extends WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.repository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||
this.repository.setAllowMultipleAuthorizationRequests(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndLoad = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.loadAuthorizationRequest(oldExchange));
|
||||
StepVerifier.create(saveAndSaveAndLoad)
|
||||
.expectNext(oldAuthorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-5145
|
||||
@Test
|
||||
public void loadAuthorizationRequestWhenSavedWithAllowMultipleAuthorizationRequestsThenReturnOldAuthorizationRequest() {
|
||||
// save 2 requests with legacy (allowMultipleAuthorizationRequests=true) and load
|
||||
// with new
|
||||
WebSessionOAuth2ServerAuthorizationRequestRepository legacy = new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||
legacy.setAllowMultipleAuthorizationRequests(true);
|
||||
// @formatter:off
|
||||
String state1 = "state-1122";
|
||||
OAuth2AuthorizationRequest authorizationRequest1 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state1)
|
||||
.build();
|
||||
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest1, this.exchange))
|
||||
.verifyComplete();
|
||||
String state2 = "state-3344";
|
||||
OAuth2AuthorizationRequest authorizationRequest2 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state2)
|
||||
.build();
|
||||
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest2, this.exchange))
|
||||
.verifyComplete();
|
||||
ServerHttpRequest newRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, state1)
|
||||
.build();
|
||||
ServerWebExchange newExchange = this.exchange.mutate()
|
||||
.request(newRequest)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange))
|
||||
.expectNext(authorizationRequest1)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-5145
|
||||
@Test
|
||||
public void saveAuthorizationRequestWhenSavedWithAllowMultipleAuthorizationRequestsThenLoadNewAuthorizationRequest() {
|
||||
// save 2 requests with legacy (allowMultipleAuthorizationRequests=true), save
|
||||
// with new, and load with new
|
||||
WebSessionOAuth2ServerAuthorizationRequestRepository legacy = new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||
legacy.setAllowMultipleAuthorizationRequests(true);
|
||||
// @formatter:off
|
||||
String state1 = "state-1122";
|
||||
OAuth2AuthorizationRequest authorizationRequest1 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state1)
|
||||
.build();
|
||||
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest1, this.exchange))
|
||||
.verifyComplete();
|
||||
String state2 = "state-3344";
|
||||
OAuth2AuthorizationRequest authorizationRequest2 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state2)
|
||||
.build();
|
||||
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest2, this.exchange))
|
||||
.verifyComplete();
|
||||
String state3 = "state-5566";
|
||||
OAuth2AuthorizationRequest authorizationRequest3 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state3)
|
||||
.build();
|
||||
ServerHttpRequest newRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, state3)
|
||||
.build();
|
||||
ServerWebExchange newExchange = this.exchange.mutate()
|
||||
.request(newRequest)
|
||||
.build();
|
||||
Mono<OAuth2AuthorizationRequest> saveAndLoad = this.repository
|
||||
.saveAuthorizationRequest(authorizationRequest3, this.exchange)
|
||||
.then(this.repository.loadAuthorizationRequest(newExchange));
|
||||
StepVerifier.create(saveAndLoad)
|
||||
.expectNext(authorizationRequest3)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAuthorizationRequestWhenMultipleThenOnlyOneRemoved() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.removeAuthorizationRequest(this.exchange));
|
||||
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(oldExchange))
|
||||
.expectNext(oldAuthorizationRequest)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-7327
|
||||
@Test
|
||||
public void removeAuthorizationRequestWhenMultipleThenRemovedAndSessionAttributeUpdated() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
Map<String, Object> sessionAttrs = spy(new HashMap<>());
|
||||
WebSession session = mock(WebSession.class);
|
||||
given(session.getAttributes()).willReturn(sessionAttrs);
|
||||
WebSessionManager sessionManager = (e) -> Mono.just(session);
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.removeAuthorizationRequest(this.exchange));
|
||||
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
verify(sessionAttrs, times(3)).put(any(), any());
|
||||
}
|
||||
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebSessionOAuth2ServerAuthorizationRequestRepository} when
|
||||
* {@link WebSessionOAuth2ServerAuthorizationRequestRepository#setAllowMultipleAuthorizationRequests(boolean)}
|
||||
* is disabled.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class WebSessionOAuth2ServerAuthorizationRequestRepositoryDoNotAllowMultipleAuthorizationRequestsTests
|
||||
extends WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.repository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||
this.repository.setAllowMultipleAuthorizationRequests(false);
|
||||
}
|
||||
|
||||
// gh-5145
|
||||
@Test
|
||||
public void loadAuthorizationRequestWhenMultipleSavedThenReturnLastAuthorizationRequest() {
|
||||
// @formatter:off
|
||||
String state1 = "state-1122";
|
||||
OAuth2AuthorizationRequest authorizationRequest1 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state1)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.saveAuthorizationRequest(authorizationRequest1, this.exchange))
|
||||
.verifyComplete();
|
||||
String state2 = "state-3344";
|
||||
OAuth2AuthorizationRequest authorizationRequest2 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state2)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.saveAuthorizationRequest(authorizationRequest2, this.exchange))
|
||||
.verifyComplete();
|
||||
String state3 = "state-5566";
|
||||
OAuth2AuthorizationRequest authorizationRequest3 = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(state3)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.saveAuthorizationRequest(authorizationRequest3, this.exchange))
|
||||
.verifyComplete();
|
||||
ServerHttpRequest newRequest1 = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, state1)
|
||||
.build();
|
||||
ServerWebExchange newExchange1 = this.exchange.mutate()
|
||||
.request(newRequest1)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange1))
|
||||
.verifyComplete();
|
||||
ServerHttpRequest newRequest2 = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, state2)
|
||||
.build();
|
||||
ServerWebExchange newExchange2 = this.exchange.mutate()
|
||||
.request(newRequest2)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange2))
|
||||
.verifyComplete();
|
||||
ServerHttpRequest newRequest3 = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, state3)
|
||||
.build();
|
||||
ServerWebExchange newExchange3 = this.exchange.mutate()
|
||||
.request(newRequest3)
|
||||
.build();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange3))
|
||||
.expectNext(authorizationRequest3)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-5145
|
||||
@Test
|
||||
public void removeAuthorizationRequestWhenMultipleThenSessionAttributeRemoved() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
Map<String, Object> sessionAttrs = spy(new HashMap<>());
|
||||
WebSession session = mock(WebSession.class);
|
||||
given(session.getAttributes()).willReturn(sessionAttrs);
|
||||
WebSessionManager sessionManager = (e) -> Mono.just(session);
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.removeAuthorizationRequest(this.exchange));
|
||||
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
verify(sessionAttrs, times(2)).put(anyString(), any());
|
||||
verify(sessionAttrs).remove(anyString());
|
||||
}
|
||||
|
||||
}
|
||||
+5
-120
@@ -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.
|
||||
@@ -16,43 +16,31 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @since 5.1
|
||||
*/
|
||||
public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
public abstract class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
|
||||
private WebSessionOAuth2ServerAuthorizationRequestRepository repository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
|
||||
protected WebSessionOAuth2ServerAuthorizationRequestRepository repository;
|
||||
|
||||
// @formatter:off
|
||||
private OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
protected OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
@@ -60,7 +48,7 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
private ServerWebExchange exchange = MockServerWebExchange
|
||||
protected ServerWebExchange exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/").queryParam(OAuth2ParameterNames.STATE, "state"));
|
||||
|
||||
@Test
|
||||
@@ -114,39 +102,6 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndLoad = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.loadAuthorizationRequest(oldExchange));
|
||||
StepVerifier.create(saveAndSaveAndLoad)
|
||||
.expectNext(oldAuthorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveAuthorizationRequestWhenAuthorizationRequestNullThenThrowsIllegalArgumentException() {
|
||||
this.authorizationRequest = null;
|
||||
@@ -211,76 +166,6 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAuthorizationRequestWhenMultipleThenOnlyOneRemoved() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.removeAuthorizationRequest(this.exchange));
|
||||
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(oldExchange))
|
||||
.expectNext(oldAuthorizationRequest)
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-7327
|
||||
@Test
|
||||
public void removeAuthorizationRequestWhenMultipleThenRemovedAndSessionAttributeUpdated() {
|
||||
String oldState = "state0";
|
||||
// @formatter:off
|
||||
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
|
||||
.queryParam(OAuth2ParameterNames.STATE, oldState)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri("https://example.com/oauth2/authorize")
|
||||
.clientId("client-id")
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state(oldState)
|
||||
.build();
|
||||
// @formatter:on
|
||||
Map<String, Object> sessionAttrs = spy(new HashMap<>());
|
||||
WebSession session = mock(WebSession.class);
|
||||
given(session.getAttributes()).willReturn(sessionAttrs);
|
||||
WebSessionManager sessionManager = (e) -> Mono.just(session);
|
||||
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
|
||||
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
|
||||
// @formatter:off
|
||||
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
|
||||
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
|
||||
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
|
||||
.then(this.repository.removeAuthorizationRequest(this.exchange));
|
||||
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
|
||||
.verifyComplete();
|
||||
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
verify(sessionAttrs, times(3)).put(any(), any());
|
||||
}
|
||||
|
||||
private void assertSessionStartedIs(boolean expected) {
|
||||
// @formatter:off
|
||||
Mono<Boolean> isStarted = this.exchange.getSession()
|
||||
|
||||
+9
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
* Copyright 2019-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.
|
||||
@@ -91,14 +91,17 @@ class PayloadInterceptorRSocket extends RSocketProxy {
|
||||
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
|
||||
return Flux.from(payloads).switchOnFirst((signal, innerFlux) -> {
|
||||
Payload firstPayload = signal.get();
|
||||
return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload).flatMapMany((context) -> innerFlux
|
||||
.skip(1).flatMap((p) -> intercept(PayloadExchangeType.PAYLOAD, p).thenReturn(p))
|
||||
.transform((securedPayloads) -> Flux.concat(Flux.just(firstPayload), securedPayloads))
|
||||
.transform((securedPayloads) -> this.source.requestChannel(securedPayloads))
|
||||
.subscriberContext(context));
|
||||
return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload).flatMapMany(
|
||||
(context) -> innerFlux.index().concatMap((tuple) -> justOrIntercept(tuple.getT1(), tuple.getT2()))
|
||||
.transform((securedPayloads) -> this.source.requestChannel(securedPayloads))
|
||||
.subscriberContext(context));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Payload> justOrIntercept(Long index, Payload payload) {
|
||||
return (index == 0) ? Mono.just(payload) : intercept(PayloadExchangeType.PAYLOAD, payload).thenReturn(payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> metadataPush(Payload payload) {
|
||||
return intercept(PayloadExchangeType.METADATA_PUSH, payload)
|
||||
|
||||
+61
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
* Copyright 2019-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.
|
||||
@@ -19,10 +19,14 @@ package org.springframework.security.rsocket.core;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.RSocket;
|
||||
import io.rsocket.metadata.WellKnownMimeType;
|
||||
import io.rsocket.util.ByteBufPayload;
|
||||
import io.rsocket.util.DefaultPayload;
|
||||
import io.rsocket.util.RSocketProxy;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -32,13 +36,17 @@ import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.test.publisher.PublisherProbe;
|
||||
import reactor.test.publisher.TestPublisher;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
@@ -56,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
@@ -265,6 +274,57 @@ public class PayloadInterceptorRSocketTests {
|
||||
verify(this.delegate).requestChannel(any());
|
||||
}
|
||||
|
||||
// gh-9345
|
||||
@Test
|
||||
public void requestChannelWhenInterceptorCompletesThenAllPayloadsRetained() {
|
||||
ExecutorService executors = Executors.newSingleThreadExecutor();
|
||||
Payload payload = ByteBufPayload.create("data");
|
||||
Payload payloadTwo = ByteBufPayload.create("moredata");
|
||||
Payload payloadThree = ByteBufPayload.create("stillmoredata");
|
||||
Context ctx = Context.empty();
|
||||
Flux<Payload> payloads = this.payloadResult.flux();
|
||||
given(this.interceptor.intercept(any(), any())).willReturn(Mono.empty())
|
||||
.willReturn(Mono.error(() -> new AccessDeniedException("Access Denied")));
|
||||
given(this.delegate.requestChannel(any())).willAnswer((invocation) -> {
|
||||
Flux<Payload> input = invocation.getArgument(0);
|
||||
return Flux.from(input).switchOnFirst((signal, innerFlux) -> innerFlux.map(Payload::getDataUtf8)
|
||||
.transform((data) -> Flux.<String>create((emitter) -> {
|
||||
Runnable run = () -> data.subscribe(new CoreSubscriber<String>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
s.request(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(String s) {
|
||||
emitter.next(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
emitter.error(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
emitter.complete();
|
||||
}
|
||||
});
|
||||
executors.execute(run);
|
||||
})).map(DefaultPayload::create));
|
||||
});
|
||||
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
|
||||
Arrays.asList(this.interceptor), this.metadataMimeType, this.dataMimeType, ctx);
|
||||
StepVerifier.create(interceptor.requestChannel(payloads).doOnDiscard(Payload.class, Payload::release))
|
||||
.then(() -> this.payloadResult.assertSubscribers())
|
||||
.then(() -> this.payloadResult.emit(payload, payloadTwo, payloadThree))
|
||||
.assertNext((next) -> assertThat(next.getDataUtf8()).isEqualTo(payload.getDataUtf8()))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
verify(this.interceptor, times(2)).intercept(this.exchange.capture(), any());
|
||||
assertThat(this.exchange.getValue().getPayload()).isEqualTo(payloadTwo);
|
||||
verify(this.delegate).requestChannel(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestChannelWhenInterceptorErrorsThenDelegateNotSubscribed() {
|
||||
RuntimeException expected = new RuntimeException("Oops");
|
||||
|
||||
@@ -43,9 +43,9 @@ dependencies {
|
||||
api "org.opensaml:opensaml-core"
|
||||
api "org.opensaml:opensaml-saml-api"
|
||||
api "org.opensaml:opensaml-saml-impl"
|
||||
opensaml4MainCompile "org.opensaml:opensaml-core:4.1.0"
|
||||
opensaml4MainCompile "org.opensaml:opensaml-saml-api:4.1.0"
|
||||
opensaml4MainCompile "org.opensaml:opensaml-saml-impl:4.1.0"
|
||||
opensaml4MainCompile "org.opensaml:opensaml-core:4.1.1"
|
||||
opensaml4MainCompile "org.opensaml:opensaml-saml-api:4.1.1"
|
||||
opensaml4MainCompile "org.opensaml:opensaml-saml-impl:4.1.1"
|
||||
|
||||
provided 'javax.servlet:javax.servlet-api'
|
||||
|
||||
|
||||
+3
-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.
|
||||
@@ -29,7 +29,6 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -82,6 +81,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
|
||||
@@ -663,7 +663,7 @@ public final class SecurityMockServerConfigurers {
|
||||
return getAuthorities((Collection) scope);
|
||||
}
|
||||
String scopes = scope.toString();
|
||||
if (StringUtils.isBlank(scopes)) {
|
||||
if (!StringUtils.hasText(scopes)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return getAuthorities(Arrays.asList(scopes.split(" ")));
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.
|
||||
@@ -348,7 +348,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
}
|
||||
return;
|
||||
}
|
||||
httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context);
|
||||
httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication);
|
||||
// If HttpSession exists, store current SecurityContext but only if it has
|
||||
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
|
||||
if (httpSession != null) {
|
||||
@@ -369,8 +369,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
|| context.getAuthentication() != this.authBeforeExecution;
|
||||
}
|
||||
|
||||
private HttpSession createNewSessionIfAllowed(SecurityContext context) {
|
||||
if (isTransientAuthentication(context.getAuthentication())) {
|
||||
private HttpSession createNewSessionIfAllowed(SecurityContext context, Authentication authentication) {
|
||||
if (isTransientAuthentication(authentication)) {
|
||||
return null;
|
||||
}
|
||||
if (this.httpSessionExistedAtStartOfRequest) {
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.
|
||||
@@ -628,6 +628,22 @@ public class HttpSessionSecurityContextRepositoryTests {
|
||||
assertThat(session).isNull();
|
||||
}
|
||||
|
||||
// gh-8947
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextAuthenticationUpdatedToNullThenSkipped() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
|
||||
SomeOtherTransientAuthentication authentication = new SomeOtherTransientAuthentication();
|
||||
repo.loadContext(holder);
|
||||
SecurityContext context = mock(SecurityContext.class);
|
||||
given(context.getAuthentication()).willReturn(authentication).willReturn(null);
|
||||
repo.saveContext(context, holder.getRequest(), holder.getResponse());
|
||||
MockHttpSession session = (MockHttpSession) request.getSession(false);
|
||||
assertThat(session).isNull();
|
||||
}
|
||||
|
||||
private SecurityContext createSecurityContext(UserDetails userDetails) {
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails,
|
||||
userDetails.getPassword(), userDetails.getAuthorities());
|
||||
|
||||
Reference in New Issue
Block a user