Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6eeafb10c | |||
| 2d52fb8e4b | |||
| 37d8846652 | |||
| 74b8c6715b | |||
| 8c9275067e | |||
| 4b8bfee7c5 | |||
| b727f24c95 | |||
| d55c8aac4f | |||
| c4e9fb885d | |||
| 442faccb5f | |||
| e25117856e | |||
| c69df9fba0 | |||
| 39cee36065 | |||
| a106188add | |||
| 5eefe9dcff | |||
| 98321b769a | |||
| 3d7e22a4e9 | |||
| 84cca81edf | |||
| 094bf1b527 | |||
| 66665344c5 | |||
| 6c7703789a | |||
| fabf7f649c | |||
| 7b88ab289d | |||
| 26566af431 | |||
| 5257e36ffc | |||
| 0421e25cba | |||
| 1c885cf3a3 | |||
| 1c3ce1e401 | |||
| b851a8bf6d | |||
| faadadf71a | |||
| 2fcf48a11b | |||
| 0398d86f39 | |||
| c9112dfaab | |||
| 4156766fe0 | |||
| f125785328 | |||
| 4643c0d999 | |||
| d2d82b5e94 | |||
| d4d03ea146 | |||
| 2db4430dcd | |||
| 000b4bc495 | |||
| aad3e61f4d | |||
| 51d5fb9c03 | |||
| 7db357d36a | |||
| f5bc6ce665 | |||
| 4cbb057b97 | |||
| c0f7cecc6d | |||
| 22ffa833ca | |||
| f41b77a4db | |||
| 2028507bf8 | |||
| ffdb397830 | |||
| 225dc593a8 | |||
| 3cfaf0d11d | |||
| fbfa13bd47 | |||
| 0c6b427132 | |||
| 0f7d17bae6 | |||
| c11248b589 | |||
| 972075677f | |||
| 11614edcfe | |||
| 2461d00f4d | |||
| eec6729125 | |||
| fc9c2513d8 | |||
| 1f3c4707f3 | |||
| 579fc565ae | |||
| 5cf23585c0 | |||
| 50d793eab2 | |||
| c715df687a | |||
| 4ddecdbe36 | |||
| 94c8830f76 | |||
| 885f1d3165 | |||
| e414d2bd5c | |||
| dbcc3130ee | |||
| ac9c398fea | |||
| 7c428fb75e | |||
| aa1934ee95 | |||
| 58e8fe837f | |||
| fc25b87967 | |||
| f39f215140 | |||
| 626e53d121 | |||
| a5464ed819 | |||
| 667cab6cda | |||
| 52c7141aac | |||
| e23c1cf7a7 | |||
| 14a48ea939 | |||
| 709de43e89 | |||
| c2e86ad067 | |||
| 9c6d90490d | |||
| 9b3f834bff | |||
| 70bfc39418 | |||
| 621889fa18 | |||
| fe252f5057 |
@@ -1,5 +0,0 @@
|
||||
REPOSITORY_REF="$1"
|
||||
TOKEN="$2"
|
||||
|
||||
curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build-reference"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches
|
||||
echo "Requested Build for $REPOSITORY_REF"
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Generate Antora Files and Request Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
tags: '**'
|
||||
|
||||
env:
|
||||
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Generate antora.yml
|
||||
run: ./gradlew :spring-security-docs:generateAntora
|
||||
- name: Extract Branch Name
|
||||
id: extract_branch_name
|
||||
run: echo "##[set-output name=generated_branch_name;]$(echo ${GITHUB_REPOSITORY}/${GITHUB_REF##*/})"
|
||||
- name: Push generated antora files to the spring-generated-docs
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.4
|
||||
with:
|
||||
branch: ${{ steps.extract_branch_name.outputs.generated_branch_name }} # The branch the action should deploy to.
|
||||
folder: "docs/build/generateAntora" # The folder the action should deploy.
|
||||
repository-name: "spring-io/spring-generated-docs"
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Dispatch Build Request
|
||||
run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-projects/spring-security' "$GH_ACTIONS_REPO_TOKEN"
|
||||
@@ -31,15 +31,15 @@ jobs:
|
||||
runjobs: ${{ steps.continue.outputs.runjobs }}
|
||||
project_version: ${{ steps.continue.outputs.project_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- id: continue
|
||||
name: Determine if should continue
|
||||
run: |
|
||||
# Run jobs if in upstream repository
|
||||
echo "::set-output name=runjobs::true"
|
||||
echo "runjobs=true" >>$GITHUB_OUTPUT
|
||||
# Extract version from gradle.properties
|
||||
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
|
||||
echo "::set-output name=project_version::$version"
|
||||
echo "project_version=$version" >>$GITHUB_OUTPUT
|
||||
build_jdk_11:
|
||||
name: Build JDK 11
|
||||
needs: [prerequisites]
|
||||
@@ -49,12 +49,13 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: 'gradle'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Set up gradle user name
|
||||
@@ -71,7 +72,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
@@ -89,7 +90,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
@@ -113,7 +114,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
@@ -130,7 +131,7 @@ jobs:
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
@@ -154,7 +155,7 @@ jobs:
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
@@ -175,7 +176,7 @@ jobs:
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
@@ -205,7 +206,7 @@ jobs:
|
||||
TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Set up gradle
|
||||
@@ -286,7 +287,7 @@ jobs:
|
||||
TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [ gh-pages ]
|
||||
tags: '**'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- id: checkout-source
|
||||
name: Checkout Source Code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Set up gradle
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew gitHubCheckNextVersionDueToday
|
||||
echo "::set-output name=is_due_today::$(cat build/github/milestones/is-due-today)"
|
||||
echo "is_due_today=$(cat build/github/milestones/is-due-today)" >>$GITHUB_OUTPUT
|
||||
- id: check-open-issues
|
||||
name: Check for open issues
|
||||
if: steps.check-release-due.outputs.is_due_today == 'true'
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew gitHubCheckMilestoneHasNoOpenIssues
|
||||
echo "::set-output name=is_open_issues::$(cat build/github/milestones/is-open-issues)"
|
||||
echo "is_open_issues=$(cat build/github/milestones/is-open-issues)" >>$GITHUB_OUTPUT
|
||||
- id: validate-release-state
|
||||
name: Validate State of Release
|
||||
if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'true'
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ See also the https://github.com/spring-projects/spring-framework/wiki/Gradle-bui
|
||||
|
||||
== Getting Support
|
||||
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
|
||||
https://spring.io/services[Commercial support] is available too.
|
||||
https://spring.io/support[Commercial support] is available too.
|
||||
|
||||
== Contributing
|
||||
https://help.github.com/articles/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc[contributor guidelines] for details.
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import io.spring.gradle.IncludeRepoTask
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.11'
|
||||
classpath "io.freefair.gradle:aspectj-plugin:6.4.3.1"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
|
||||
@@ -93,7 +93,7 @@ dependencies {
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
implementation 'com.github.spullara.mustache.java:compiler:0.9.4'
|
||||
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.10'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.11'
|
||||
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0'
|
||||
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
|
||||
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0'
|
||||
|
||||
@@ -36,7 +36,7 @@ class CheckstylePlugin implements Plugin<Project> {
|
||||
if (checkstyleDir.exists() && checkstyleDir.directory) {
|
||||
project.getPluginManager().apply('checkstyle')
|
||||
project.dependencies.add('checkstyle', 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.15')
|
||||
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.10')
|
||||
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.11')
|
||||
|
||||
project.checkstyle {
|
||||
configDirectory = checkstyleDir
|
||||
|
||||
@@ -62,6 +62,33 @@ public class SchemaDeployPlugin implements Plugin<Project> {
|
||||
execute "rm -f $tempPath*.zip"
|
||||
execute "rm -rf $extractPath*"
|
||||
execute "mv $tempPath/* $extractPath"
|
||||
|
||||
/*
|
||||
* Copy spring-security-oauth schemas so that legacy projects using the "http" scheme can
|
||||
* resolve the following schema locations:
|
||||
*
|
||||
* - http://www.springframework.org/schema/security/spring-security-oauth-1.0.xsd
|
||||
* - http://www.springframework.org/schema/security/spring-security-oauth2-1.0.xsd
|
||||
* - http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd
|
||||
*
|
||||
* Note: The directory:
|
||||
* https://www.springframework.org/schema/security/
|
||||
*
|
||||
* is a symbolic link pointing to:
|
||||
* https://docs.spring.io/autorepo/schema/spring-security/current/security/
|
||||
*
|
||||
* which in turn points to the latest:
|
||||
* https://docs.spring.io/autorepo/schema/spring-security/$version/security/
|
||||
*
|
||||
* with the help of the autoln command which is run regularly via a cron job.
|
||||
*
|
||||
* See https://github.com/spring-io/autoln for more info.
|
||||
*/
|
||||
if (name == "spring-security") {
|
||||
def springSecurityOauthPath = "/var/www/domains/spring.io/docs/htdocs/autorepo/schema/spring-security-oauth/current/security"
|
||||
execute "cp $springSecurityOauthPath/* ${extractPath}security"
|
||||
}
|
||||
|
||||
execute "chmod -R g+w $extractPath"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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.gradle.github.user;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class GitHubUserApi {
|
||||
private String baseUrl = "https://api.github.com";
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public GitHubUserApi(String gitHubAccessToken) {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.addInterceptor(new AuthorizationInterceptor(gitHubAccessToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a GitHub user by the personal access token.
|
||||
*
|
||||
* @return The GitHub user
|
||||
*/
|
||||
public User getUser() {
|
||||
String url = this.baseUrl + "/user";
|
||||
Request request = new Request.Builder().url(url).get().build();
|
||||
try (Response response = this.httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new RuntimeException(
|
||||
String.format("Unable to retrieve GitHub user." +
|
||||
" Please check the personal access token and try again." +
|
||||
" Got response %s", response));
|
||||
}
|
||||
return this.gson.fromJson(response.body().charStream(), User.class);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Unable to retrieve GitHub user.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthorizationInterceptor implements Interceptor {
|
||||
private final String token;
|
||||
|
||||
public AuthorizationInterceptor(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Bearer " + this.token)
|
||||
.build();
|
||||
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.springframework.gradle.github.user;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class User {
|
||||
private Long id;
|
||||
private String login;
|
||||
private String name;
|
||||
private String url;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return this.login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", login='" + login + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", url='" + url + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@ import java.util.Base64;
|
||||
* Implements necessary calls to the Sagan API See https://spring.io/restdocs/index.html
|
||||
*/
|
||||
public class SaganApi {
|
||||
private String baseUrl = "https://spring.io/api";
|
||||
private String baseUrl = "https://api.spring.io";
|
||||
|
||||
private OkHttpClient client;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public SaganApi(String gitHubToken) {
|
||||
public SaganApi(String username, String gitHubToken) {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new BasicInterceptor("not-used", gitHubToken))
|
||||
.addInterceptor(new BasicInterceptor(username, gitHubToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -20,6 +20,9 @@ import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.gradle.github.user.GitHubUserApi;
|
||||
import org.springframework.gradle.github.user.User;
|
||||
|
||||
public class SaganCreateReleaseTask extends DefaultTask {
|
||||
|
||||
@Input
|
||||
@@ -35,11 +38,22 @@ public class SaganCreateReleaseTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void saganCreateRelease() {
|
||||
SaganApi sagan = new SaganApi(this.gitHubAccessToken);
|
||||
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
|
||||
User user = github.getUser();
|
||||
|
||||
// Antora reference docs URLs for snapshots do not contain -SNAPSHOT
|
||||
String referenceDocUrl = this.referenceDocUrl;
|
||||
if (this.version.endsWith("-SNAPSHOT")) {
|
||||
referenceDocUrl = this.referenceDocUrl
|
||||
.replace("{version}", this.version)
|
||||
.replace("-SNAPSHOT", "");
|
||||
}
|
||||
|
||||
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
|
||||
Release release = new Release();
|
||||
release.setVersion(this.version);
|
||||
release.setApiDocUrl(this.apiDocUrl);
|
||||
release.setReferenceDocUrl(this.referenceDocUrl);
|
||||
release.setReferenceDocUrl(referenceDocUrl);
|
||||
sagan.createReleaseForProject(release, this.projectName);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.gradle.github.user.GitHubUserApi;
|
||||
import org.springframework.gradle.github.user.User;
|
||||
|
||||
public class SaganDeleteReleaseTask extends DefaultTask {
|
||||
|
||||
@Input
|
||||
@@ -31,7 +34,10 @@ public class SaganDeleteReleaseTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void saganCreateRelease() {
|
||||
SaganApi sagan = new SaganApi(this.gitHubAccessToken);
|
||||
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
|
||||
User user = github.getUser();
|
||||
|
||||
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
|
||||
sagan.deleteReleaseForProject(this.version, this.projectName);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public class SaganApiTests {
|
||||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.sagan = new SaganApi("mock-oauth-token");
|
||||
this.sagan = new SaganApi("user", "mock-oauth-token");
|
||||
this.baseUrl = this.server.url("/api").toString();
|
||||
this.sagan.setBaseUrl(this.baseUrl);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class SaganApiTests {
|
||||
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
|
||||
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases");
|
||||
assertThat(request.getMethod()).isEqualToIgnoringCase("post");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg==");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
|
||||
assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" +
|
||||
" \"version\":\"5.6.0-SNAPSHOT\",\n" +
|
||||
" \"current\":false,\n" +
|
||||
@@ -79,7 +79,7 @@ public class SaganApiTests {
|
||||
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
|
||||
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases/5.6.0-SNAPSHOT");
|
||||
assertThat(request.getMethod()).isEqualToIgnoringCase("delete");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg==");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
|
||||
assertThat(request.getBody().readString(Charset.defaultCharset())).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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.gradle.github.user;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class GitHubUserApiTests {
|
||||
private GitHubUserApi gitHubUserApi;
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.baseUrl = this.server.url("/api").toString();
|
||||
this.gitHubUserApi = new GitHubUserApi("mock-oauth-token");
|
||||
this.gitHubUserApi.setBaseUrl(this.baseUrl);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserWhenValidParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String responseJson = "{\n" +
|
||||
" \"avatar_url\": \"https://avatars.githubusercontent.com/u/583231?v=4\",\n" +
|
||||
" \"bio\": null,\n" +
|
||||
" \"blog\": \"https://github.blog\",\n" +
|
||||
" \"company\": \"@github\",\n" +
|
||||
" \"created_at\": \"2011-01-25T18:44:36Z\",\n" +
|
||||
" \"email\": null,\n" +
|
||||
" \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n" +
|
||||
" \"followers\": 8481,\n" +
|
||||
" \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n" +
|
||||
" \"following\": 9,\n" +
|
||||
" \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n" +
|
||||
" \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n" +
|
||||
" \"gravatar_id\": \"\",\n" +
|
||||
" \"hireable\": null,\n" +
|
||||
" \"html_url\": \"https://github.com/octocat\",\n" +
|
||||
" \"id\": 583231,\n" +
|
||||
" \"location\": \"San Francisco\",\n" +
|
||||
" \"login\": \"octocat\",\n" +
|
||||
" \"name\": \"The Octocat\",\n" +
|
||||
" \"node_id\": \"MDQ6VXNlcjU4MzIzMQ==\",\n" +
|
||||
" \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n" +
|
||||
" \"public_gists\": 8,\n" +
|
||||
" \"public_repos\": 8,\n" +
|
||||
" \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n" +
|
||||
" \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n" +
|
||||
" \"site_admin\": false,\n" +
|
||||
" \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n" +
|
||||
" \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n" +
|
||||
" \"twitter_username\": null,\n" +
|
||||
" \"type\": \"User\",\n" +
|
||||
" \"updated_at\": \"2023-02-25T12:14:58Z\",\n" +
|
||||
" \"url\": \"https://api.github.com/users/octocat\"\n" +
|
||||
"}";
|
||||
// @formatter:on
|
||||
this.server.enqueue(new MockResponse().setBody(responseJson));
|
||||
|
||||
User user = this.gitHubUserApi.getUser();
|
||||
assertThat(user.getId()).isEqualTo(583231);
|
||||
assertThat(user.getLogin()).isEqualTo("octocat");
|
||||
assertThat(user.getName()).isEqualTo("The Octocat");
|
||||
assertThat(user.getUrl()).isEqualTo("https://api.github.com/users/octocat");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserWhenErrorResponseThenException() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(400));
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(RuntimeException.class)
|
||||
.isThrownBy(() -> this.gitHubUserApi.getUser());
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -3254,7 +3254,12 @@ 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;
|
||||
Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter);
|
||||
if (registeredFilterOrder == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"The Filter class " + registeredFilter.getName() + " does not have a registered order");
|
||||
}
|
||||
int order = registeredFilterOrder + offset;
|
||||
this.filters.add(new OrderedFilter(filter, order));
|
||||
this.filterOrders.put(filter.getClass(), order);
|
||||
return this;
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ import org.springframework.security.web.csrf.CsrfToken;
|
||||
*
|
||||
* <h2>Shared Objects Created</h2>
|
||||
*
|
||||
* No shared objects are created. isLogoutRequest
|
||||
* No shared objects are created.
|
||||
*
|
||||
* <h2>Shared Objects Used</h2>
|
||||
*
|
||||
* The following shared objects are used:
|
||||
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -35,6 +35,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -325,6 +327,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* @return the {@link LogoutFilter} to use.
|
||||
*/
|
||||
private LogoutFilter createLogoutFilter(H http) {
|
||||
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
|
||||
this.logoutHandlers.add(this.contextLogoutHandler);
|
||||
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
|
||||
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
|
||||
@@ -334,6 +337,14 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return result;
|
||||
}
|
||||
|
||||
private SecurityContextRepository getSecurityContextRepository(H http) {
|
||||
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
|
||||
if (securityContextRepository == null) {
|
||||
securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
}
|
||||
return securityContextRepository;
|
||||
}
|
||||
|
||||
private RequestMatcher getLogoutRequestMatcher(H http) {
|
||||
if (this.logoutRequestMatcher != null) {
|
||||
return this.logoutRequestMatcher;
|
||||
|
||||
+1
-1
@@ -288,7 +288,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
/**
|
||||
* Controls the maximum number of sessions for a user. The default is to allow any
|
||||
* number of users.
|
||||
* number of sessions.
|
||||
* @param maximumSessions the maximum number of sessions for a user
|
||||
* @return the {@link SessionManagementConfigurer} for further customizations
|
||||
*/
|
||||
|
||||
+5
-4
@@ -49,10 +49,11 @@ import org.springframework.util.Assert;
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* protected void configure(HttpSecurity http) throws Exception {
|
||||
* http.apply(new UrlAuthorizationConfigurer<HttpSecurity>()).getRegistry()
|
||||
* .antMatchers("/users**", "/sessions/**").hasRole("USER")
|
||||
* .antMatchers("/signup").hasRole("ANONYMOUS").anyRequest().hasRole("USER");
|
||||
* @Bean
|
||||
* public SecurityFilterChain filterChain(HttpSecurity http, ApplicationContext context) throws Exception {
|
||||
* http.apply(new UrlAuthorizationConfigurer<HttpSecurity>(context)).getRegistry()
|
||||
* .requestMatchers("/users**", "/sessions/**").hasRole("USER")
|
||||
* .requestMatchers("/signup").hasRole("ANONYMOUS").anyRequest().hasRole("USER");
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
|
||||
+37
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -208,30 +208,49 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
ParserContext parserContext) {
|
||||
String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID);
|
||||
String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION);
|
||||
RelyingPartyRegistration.Builder builder;
|
||||
if (StringUtils.hasText(metadataLocation)) {
|
||||
builder = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId);
|
||||
}
|
||||
else {
|
||||
builder = RelyingPartyRegistration.withRegistrationId(registrationId)
|
||||
.assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt,
|
||||
assertingParties, apBuilder, parserContext));
|
||||
}
|
||||
addRemainingProperties(relyingPartyRegistrationElt, builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void addRemainingProperties(Element relyingPartyRegistrationElt,
|
||||
RelyingPartyRegistration.Builder builder) {
|
||||
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
|
||||
String singleLogoutServiceLocation = relyingPartyRegistrationElt
|
||||
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
|
||||
String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt
|
||||
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
|
||||
Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt);
|
||||
if (StringUtils.hasText(metadataLocation)) {
|
||||
return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
|
||||
.singleLogoutServiceBinding(singleLogoutServiceBinding);
|
||||
}
|
||||
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
|
||||
String assertionConsumerServiceLocation = relyingPartyRegistrationElt
|
||||
.getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION);
|
||||
Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding(
|
||||
relyingPartyRegistrationElt);
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId)
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.assertionConsumerServiceBinding(assertionConsumerServiceBinding)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
|
||||
.singleLogoutServiceBinding(singleLogoutServiceBinding)
|
||||
.assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
|
||||
builder, parserContext));
|
||||
if (StringUtils.hasText(entityId)) {
|
||||
builder.entityId(entityId);
|
||||
}
|
||||
if (StringUtils.hasText(singleLogoutServiceLocation)) {
|
||||
builder.singleLogoutServiceLocation(singleLogoutServiceLocation);
|
||||
}
|
||||
if (StringUtils.hasText(singleLogoutServiceResponseLocation)) {
|
||||
builder.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation);
|
||||
}
|
||||
if (singleLogoutServiceBinding != null) {
|
||||
builder.singleLogoutServiceBinding(singleLogoutServiceBinding);
|
||||
}
|
||||
if (StringUtils.hasText(assertionConsumerServiceLocation)) {
|
||||
builder.assertionConsumerServiceLocation(assertionConsumerServiceLocation);
|
||||
}
|
||||
if (assertionConsumerServiceBinding != null) {
|
||||
builder.assertionConsumerServiceBinding(assertionConsumerServiceBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildAssertingParty(Element relyingPartyElt, Map<String, Map<String, Object>> assertingParties,
|
||||
@@ -309,7 +328,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
if (StringUtils.hasText(assertionConsumerServiceBinding)) {
|
||||
return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding);
|
||||
}
|
||||
return Saml2MessageBinding.REDIRECT;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) {
|
||||
@@ -317,7 +336,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
if (StringUtils.hasText(singleLogoutServiceBinding)) {
|
||||
return Saml2MessageBinding.valueOf(singleLogoutServiceBinding);
|
||||
}
|
||||
return Saml2MessageBinding.POST;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) {
|
||||
|
||||
+47
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,6 +30,7 @@ import org.assertj.core.api.ListAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.UnsatisfiedDependencyException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
@@ -45,12 +46,29 @@ import org.springframework.security.web.context.request.async.WebAsyncManagerInt
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class HttpSecurityAddFilterTest {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Test
|
||||
public void addFilterAfterFilterNotRegisteredYetThenThrowIllegalArgument() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MyOtherFilterAfterMyFilterNotRegisteredYetConfig.class).autowire())
|
||||
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterBeforeFilterNotRegisteredYetThenThrowIllegalArgument() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MyOtherFilterBeforeMyFilterNotRegisteredYetConfig.class).autowire())
|
||||
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterAfterWhenSameFilterDifferentPlacesThenOrderCorrect() {
|
||||
this.spring.register(MyFilterMultipleAfterConfig.class).autowire();
|
||||
@@ -209,6 +227,34 @@ public class HttpSecurityAddFilterTest {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterAfterMyFilterNotRegisteredYetConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAfter(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterBeforeMyFilterNotRegisteredYetConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterRelativeToMyFilterBeforeConfig {
|
||||
|
||||
|
||||
+85
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 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.config.annotation.web.configurers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -23,7 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -31,9 +37,12 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -42,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
@@ -302,6 +312,80 @@ public class LogoutConfigurerTests {
|
||||
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
|
||||
CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
|
||||
this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder logoutRequest = post("/logout")
|
||||
.with(csrf())
|
||||
.with(user("user"))
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
|
||||
this.mvc.perform(logoutRequest)
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"));
|
||||
// @formatter:on
|
||||
int invocationCount = 2; // 1 from user() post processor and 1 from
|
||||
// SecurityContextLogoutHandler
|
||||
verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
|
||||
any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
|
||||
this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
|
||||
MockHttpSession session = mock(MockHttpSession.class);
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder logoutRequest = post("/logout")
|
||||
.with(csrf())
|
||||
.with(user("user"))
|
||||
.session(session)
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
|
||||
this.mvc.perform(logoutRequest)
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"))
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class InvalidateHttpSessionFalseConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout((logout) -> logout.invalidateHttpSession(false))
|
||||
.securityContext((context) -> context.requireExplicitSave(true));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CustomSecurityContextRepositoryConfig {
|
||||
|
||||
static SecurityContextRepository repository;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout(Customizer.withDefaults())
|
||||
.securityContext((context) -> context
|
||||
.requireExplicitSave(true)
|
||||
.securityContextRepository(repository)
|
||||
);
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
|
||||
+57
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -62,6 +62,27 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
"</b:beans>\n";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String METADATA_LOCATION_OVERRIDE_PROPERTIES_XML_CONFIG = "<b:beans xmlns:b=\"http://www.springframework.org/schema/beans\"\n" +
|
||||
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
|
||||
" xmlns=\"http://www.springframework.org/schema/security\"\n" +
|
||||
" xsi:schemaLocation=\"\n" +
|
||||
"\t\t\thttp://www.springframework.org/schema/security\n" +
|
||||
"\t\t\thttps://www.springframework.org/schema/security/spring-security.xsd\n" +
|
||||
"\t\t\thttp://www.springframework.org/schema/beans\n" +
|
||||
"\t\t\thttps://www.springframework.org/schema/beans/spring-beans.xsd\">\n" +
|
||||
" \n" +
|
||||
" <relying-party-registrations>\n" +
|
||||
" <relying-party-registration registration-id=\"one\"\n" +
|
||||
" entity-id=\"https://rp.example.org\"\n" +
|
||||
" metadata-location=\"${metadata-location}\"\n" +
|
||||
" assertion-consumer-service-location=\"https://rp.example.org/location\"\n" +
|
||||
" assertion-consumer-service-binding=\"REDIRECT\"/>" +
|
||||
" </relying-party-registrations>\n" +
|
||||
"\n" +
|
||||
"</b:beans>\n";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String METADATA_RESPONSE = "<?xml version=\"1.0\"?>\n" +
|
||||
"<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" entityID=\"https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php\" ID=\"_e793a707d3e1a9ee6cbec7454fdad2c7cd793dd3703179a527b9620a6e9682af\"><ds:Signature>\n" +
|
||||
@@ -143,6 +164,41 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
.containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWhenMetadataLocationConfiguredAndRegistrationHasPropertiesThenDoNotOverrideSpecifiedProperties()
|
||||
throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String serverUrl = this.server.url("/").toString();
|
||||
this.server.enqueue(xmlResponse(METADATA_RESPONSE));
|
||||
String metadataConfig = METADATA_LOCATION_OVERRIDE_PROPERTIES_XML_CONFIG.replace("${metadata-location}",
|
||||
serverUrl);
|
||||
this.spring.context(metadataConfig).autowire();
|
||||
assertThat(this.relyingPartyRegistrationRepository)
|
||||
.isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class);
|
||||
RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository
|
||||
.findByRegistrationId("one");
|
||||
RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration
|
||||
.getAssertingPartyDetails();
|
||||
assertThat(relyingPartyRegistration).isNotNull();
|
||||
assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one");
|
||||
assertThat(relyingPartyRegistration.getEntityId()).isEqualTo("https://rp.example.org");
|
||||
assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation())
|
||||
.isEqualTo("https://rp.example.org/location");
|
||||
assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding())
|
||||
.isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
assertThat(assertingPartyDetails.getEntityId())
|
||||
.isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php");
|
||||
assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isFalse();
|
||||
assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1);
|
||||
assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1);
|
||||
assertThat(assertingPartyDetails.getSingleSignOnServiceLocation())
|
||||
.isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php");
|
||||
assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
assertThat(assertingPartyDetails.getSigningAlgorithms())
|
||||
.containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWhenSingleRelyingPartyRegistrationThenAvailableInRepository() {
|
||||
this.spring.configLocations(xml("SingleRegistration")).autowire();
|
||||
|
||||
+3
-2
@@ -40,8 +40,9 @@ import org.springframework.util.Assert;
|
||||
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* The plaintext password used to perform PasswordEncoder#matches(CharSequence,
|
||||
* String)} on when the user is not found to avoid SEC-2056.
|
||||
* The plaintext password used to perform
|
||||
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is not found
|
||||
* to avoid SEC-2056.
|
||||
*/
|
||||
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
|
||||
|
||||
|
||||
+12
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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,12 +46,15 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
/**
|
||||
* Creates an instance of {@link AuthorityAuthorizationManager} with the provided
|
||||
* authority.
|
||||
* @param role the authority to check for prefixed with "ROLE_"
|
||||
* @param role the authority to check for prefixed with "ROLE_". Role should not start
|
||||
* with "ROLE_" since it is automatically prepended already.
|
||||
* @param <T> the type of object being authorized
|
||||
* @return the new instance
|
||||
*/
|
||||
public static <T> AuthorityAuthorizationManager<T> hasRole(String role) {
|
||||
Assert.notNull(role, "role cannot be null");
|
||||
Assert.isTrue(!role.startsWith(ROLE_PREFIX), () -> role + " should not start with " + ROLE_PREFIX + " since "
|
||||
+ ROLE_PREFIX + " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
|
||||
return hasAuthority(ROLE_PREFIX + role);
|
||||
}
|
||||
|
||||
@@ -70,7 +73,8 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
/**
|
||||
* Creates an instance of {@link AuthorityAuthorizationManager} with the provided
|
||||
* authorities.
|
||||
* @param roles the authorities to check for prefixed with "ROLE_"
|
||||
* @param roles the authorities to check for prefixed with "ROLE_". Each role should
|
||||
* not start with "ROLE_" since it is automatically prepended already.
|
||||
* @param <T> the type of object being authorized
|
||||
* @return the new instance
|
||||
*/
|
||||
@@ -109,7 +113,11 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
|
||||
String[] result = new String[roles.length];
|
||||
for (int i = 0; i < roles.length; i++) {
|
||||
result[i] = rolePrefix + roles[i];
|
||||
String role = roles[i];
|
||||
Assert.isTrue(!role.startsWith(rolePrefix), () -> role + " should not start with " + rolePrefix + " since "
|
||||
+ rolePrefix
|
||||
+ " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead.");
|
||||
result[i] = rolePrefix + role;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import org.springframework.security.core.Authentication;
|
||||
* A reactive authorization manager which can determine if an {@link Authentication} has
|
||||
* access to a specific object.
|
||||
*
|
||||
* @param <T> the type of object that the authorization check is being done one.
|
||||
* @param <T> the type of object that the authorization check is being done on.
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
*/
|
||||
|
||||
+20
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -41,6 +41,15 @@ public class AuthorityAuthorizationManagerTests {
|
||||
.withMessage("role cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasRoleWhenContainRoleWithRolePrefixThenException() {
|
||||
String ROLE_PREFIX = "ROLE_";
|
||||
String ROLE_USER = ROLE_PREFIX + "USER";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasRole(ROLE_USER))
|
||||
.withMessage(ROLE_USER + " should not start with " + ROLE_PREFIX + " since " + ROLE_PREFIX
|
||||
+ " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAuthorityWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAuthority(null))
|
||||
@@ -73,6 +82,16 @@ public class AuthorityAuthorizationManagerTests {
|
||||
.withMessage("rolePrefix cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAnyRoleWhenContainRoleWithRolePrefixThenException() {
|
||||
String ROLE_PREFIX = "ROLE_";
|
||||
String ROLE_USER = ROLE_PREFIX + "USER";
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(new String[] { ROLE_USER }))
|
||||
.withMessage(ROLE_USER + " should not start with " + ROLE_PREFIX + " since " + ROLE_PREFIX
|
||||
+ " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAnyAuthorityWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null))
|
||||
|
||||
+9
-9
@@ -8,15 +8,15 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.24")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.31")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.3")
|
||||
api platform("org.junit:junit-bom:5.8.2")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.4")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.11")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.4.20221012")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.11"
|
||||
api "ch.qos.logback:logback-classic:1.2.12"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.22"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.35"
|
||||
@@ -26,7 +26,7 @@ dependencies {
|
||||
api "commons-codec:commons-codec:1.15"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.12.8"
|
||||
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
|
||||
api "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
|
||||
api "jakarta.inject:jakarta.inject-api:1.0.5"
|
||||
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7"
|
||||
@@ -44,17 +44,17 @@ dependencies {
|
||||
api "org.apache.directory.server:apacheds-protocol-shared:1.5.5"
|
||||
api "org.apache.directory.server:apacheds-server-jndi:1.5.5"
|
||||
api "org.apache.directory.shared:shared-ldap:0.9.15"
|
||||
api "org.apache.httpcomponents:httpclient:4.5.13"
|
||||
api "org.apache.httpcomponents:httpclient:4.5.14"
|
||||
api "org.aspectj:aspectjrt:$aspectjVersion"
|
||||
api "org.aspectj:aspectjweaver:$aspectjVersion"
|
||||
api "org.assertj:assertj-core:3.22.0"
|
||||
api "org.bouncycastle:bcpkix-jdk15on:1.70"
|
||||
api "org.bouncycastle:bcprov-jdk15on:1.70"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.49.v20220914"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.49.v20220914"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.51.v20230217"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.51.v20230217"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.12.Final"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
api "org.hsqldb:hsqldb:2.6.1"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
api "org.mockito:mockito-core:3.12.4"
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
/package-lock.json
|
||||
/node_modules/
|
||||
/*-antora-playbook.yml
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
/* Copyright (c) 2002-2022 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.
|
||||
*/
|
||||
const { promises: fsp } = require('fs')
|
||||
const ospath = require('path')
|
||||
|
||||
/**
|
||||
* Rewrites local content sources to support the use of linked worktrees.
|
||||
*
|
||||
* @author Dan Allen <dan@opendevise.com>
|
||||
*/
|
||||
module.exports.register = function () {
|
||||
this.once('playbookBuilt', async ({ playbook }) => {
|
||||
const expandPath = this.require('@antora/expand-path-helper')
|
||||
for (const contentSource of playbook.content.sources) {
|
||||
const { url, branches } = contentSource
|
||||
if (url.charAt() !== '.') continue
|
||||
const absdir = expandPath(url, { dot: playbook.dir })
|
||||
const gitfile = ospath.join(absdir, '.git')
|
||||
if (await fsp.stat(gitfile).then((stat) => !stat.isDirectory(), () => false)) {
|
||||
const worktreeGitdir = await fsp.readFile(gitfile, 'utf8')
|
||||
.then((contents) => contents.trimRight().substr(8))
|
||||
const worktreeBranch = await fsp.readFile(ospath.join(worktreeGitdir, 'HEAD'), 'utf8')
|
||||
.then((contents) => contents.trimRight().replace(/^ref: (?:refs\/heads\/)?/, ''))
|
||||
const reldir = ospath.relative(
|
||||
playbook.dir,
|
||||
await fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8')
|
||||
.then((contents) => {
|
||||
const gitdir = ospath.join(worktreeGitdir, contents.trimRight())
|
||||
return ospath.basename(gitdir) === '.git' ? ospath.dirname(gitdir) : gitdir
|
||||
})
|
||||
)
|
||||
contentSource.url = reldir ? `.${ospath.sep}${reldir}` : '.'
|
||||
if (!branches) continue
|
||||
contentSource.branches = (branches.constructor === Array ? branches : [branches])
|
||||
.map((pattern) => pattern.replaceAll('HEAD', worktreeBranch))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+8
-2
@@ -6,7 +6,13 @@ nav:
|
||||
ext:
|
||||
collector:
|
||||
run:
|
||||
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntora
|
||||
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntoraYml
|
||||
local: true
|
||||
scan:
|
||||
dir: ./build/generateAntora
|
||||
dir: ./build/generated-antora-resources
|
||||
asciidoc:
|
||||
attributes:
|
||||
icondir: icons
|
||||
gh-old-samples-url: 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
gh-samples-url: "https://github.com/spring-projects/spring-security-samples/tree/{gh-tag}"
|
||||
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# The purpose of this Antora playbook is to generate a preview of the docs in the current branch.
|
||||
antora:
|
||||
extensions:
|
||||
- ./antora-linked-worktree-patch.js
|
||||
- '@antora/collector-extension'
|
||||
site:
|
||||
title: Spring Security
|
||||
url: https://docs.spring.io/spring-security/reference
|
||||
content:
|
||||
sources:
|
||||
- url: ./..
|
||||
branches: HEAD
|
||||
start_path: docs
|
||||
worktrees: true
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
urls:
|
||||
latest_version_segment: ''
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
|
||||
snapshot: true
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 89 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 143 KiB |
@@ -7,6 +7,8 @@ For example:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||
|
||||
@Test
|
||||
public void messageWhenNotAuthenticated() throws Exception {
|
||||
this.rest
|
||||
@@ -67,6 +69,7 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception {
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser
|
||||
|
||||
//...
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ For example:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
this.rest
|
||||
// provide a valid CSRF token
|
||||
.mutateWith(csrf())
|
||||
@@ -18,6 +20,8 @@ this.rest
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
|
||||
|
||||
this.rest
|
||||
// provide a valid CSRF token
|
||||
.mutateWith(csrf())
|
||||
|
||||
@@ -6,6 +6,9 @@ The basic setup looks like this:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
|
||||
public class HelloWebfluxMethodApplicationTests {
|
||||
@@ -31,6 +34,9 @@ public class HelloWebfluxMethodApplicationTests {
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class])
|
||||
class HelloWebfluxMethodApplicationTests {
|
||||
|
||||
@@ -10,6 +10,7 @@ The default is that accessing the URL `/logout` will log the user out by:
|
||||
- Invalidating the HTTP Session
|
||||
- Cleaning up any RememberMe authentication that was configured
|
||||
- Clearing the `SecurityContextHolder`
|
||||
- Clearing the `SecurityContextRepository`
|
||||
- Redirect to `/login?logout`
|
||||
|
||||
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
|
||||
|
||||
@@ -48,12 +48,10 @@ image::{figures}/authorizationfilter.png[]
|
||||
|
||||
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
It wraps this in an `Supplier` in order to delay lookup.
|
||||
* image:{icondir}/number_2.png[] Second, `AuthorizationFilter` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain`.
|
||||
// FIXME: link to FilterInvocation
|
||||
* image:{icondir}/number_3.png[] Next, it passes the `Supplier<Authentication>` and `FilterInvocation` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`].
|
||||
** image:{icondir}/number_4.png[] If authorization is denied, an `AccessDeniedException` is thrown.
|
||||
* image:{icondir}/number_2.png[] Second, it passes the `Supplier<Authentication>` and the `HttpServletRequest` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`].
|
||||
** image:{icondir}/number_3.png[] If authorization is denied, an `AccessDeniedException` is thrown.
|
||||
In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`.
|
||||
** image:{icondir}/number_5.png[] If access is granted, `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
|
||||
** image:{icondir}/number_4.png[] If access is granted, `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
|
||||
|
||||
We can configure Spring Security to have different rules by adding more rules in order of precedence.
|
||||
|
||||
|
||||
@@ -547,7 +547,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Advisor customAuthorize(AuthorizationManager<MethodInvocationResult> rules) {
|
||||
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
|
||||
AnnotationMatchingPointcut pattern = new AnnotationMatchingPointcut(MySecurityAnnotation.class);
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
|
||||
return interceptor;
|
||||
@@ -563,7 +563,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun customAuthorize(rules : AuthorizationManager<MethodInvocationResult>) : Advisor {
|
||||
val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java);
|
||||
val pattern = AnnotationMatchingPointcut(MySecurityAnnotation::class.java);
|
||||
val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
|
||||
return interceptor;
|
||||
|
||||
@@ -248,7 +248,7 @@ public class TenantJWSKeySelector
|
||||
}
|
||||
|
||||
private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
|
||||
return Optional.ofNullable(this.tenantRepository.findById(tenant)) <3>
|
||||
return Optional.ofNullable(this.tenants.findById(tenant)) <3>
|
||||
.map(t -> t.getAttrbute("jwks_uri"))
|
||||
.map(this::fromUri)
|
||||
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
|
||||
|
||||
@@ -1,17 +1,97 @@
|
||||
[[servlet-saml2login-authenticate-responses]]
|
||||
= Authenticating ``<saml2:Response>``s
|
||||
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default.
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it.
|
||||
|
||||
You can configure this in a number of ways including:
|
||||
|
||||
1. Setting a clock skew to timestamp validation
|
||||
2. Mapping the response to a list of `GrantedAuthority` instances
|
||||
3. Customizing the strategy for validating assertions
|
||||
4. Customizing the strategy for decrypting response and assertion elements
|
||||
1. Changing the way the `RelyingPartyRegistration` is Looked Up
|
||||
2. Setting a clock skew to timestamp validation
|
||||
3. Mapping the response to a list of `GrantedAuthority` instances
|
||||
4. Customizing the strategy for validating assertions
|
||||
5. Customizing the strategy for decrypting response and assertion elements
|
||||
|
||||
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
||||
|
||||
[[relyingpartyregistrationresolver-apply]]
|
||||
== Changing `RelyingPartyRegistration` Lookup
|
||||
|
||||
`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`].
|
||||
|
||||
To apply a `RelyingPartyRegistrationResolver` when processing `<saml2:Response>` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) {
|
||||
return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations));
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter {
|
||||
return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations));
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default.
|
||||
If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
// ...
|
||||
.saml2Login {
|
||||
filterProcessingUrl = "/saml2/login/sso"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
and:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
|
||||
== Setting a Clock Skew
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
|
||||
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
|
||||
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
|
||||
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
|
||||
@@ -640,6 +641,16 @@ which in a deployed application would translate to
|
||||
|
||||
`+https://rp.example.com/adfs+`
|
||||
|
||||
The prevailing URI patterns are as follows:
|
||||
|
||||
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
|
||||
* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration`
|
||||
* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state
|
||||
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
|
||||
|
||||
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
|
||||
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
|
||||
|
||||
[[servlet-saml2login-rpr-credentials]]
|
||||
=== Credentials
|
||||
|
||||
@@ -712,56 +723,6 @@ resource.inputStream.use {
|
||||
[TIP]
|
||||
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
|
||||
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
=== Resolving the Relying Party from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize. Among them:
|
||||
|
||||
* You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
|
||||
* You may identify tenants in a way other than by the URI path
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
You can provide a simpler resolver that, for example, always returns the same relying party:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private final RelyingPartyRegistrationResolver delegate;
|
||||
|
||||
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
|
||||
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
return this.delegate.resolve(request, "single");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
|
||||
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
|
||||
return this.delegate.resolve(request, "single")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Then, you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce ``<saml2:AuthnRequest>``s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ``<saml2:Response>``s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
|
||||
|
||||
[NOTE]
|
||||
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
||||
|
||||
[[servlet-saml2login-rpr-duplicated]]
|
||||
=== Duplicated Relying Party Configurations
|
||||
|
||||
@@ -856,3 +817,184 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
=== Resolving the `RelyingPartyRegistration` from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize that. Among them:
|
||||
|
||||
* You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>>
|
||||
* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
[NOTE]
|
||||
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
||||
|
||||
[[relyingpartyregistrationresolver-single]]
|
||||
==== Resolving to a Single Consistent `RelyingPartyRegistration`
|
||||
|
||||
You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private final RelyingPartyRegistrationResolver delegate;
|
||||
|
||||
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
|
||||
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
return this.delegate.resolve(request, "single");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
|
||||
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
|
||||
return this.delegate.resolve(request, "single")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
|
||||
|
||||
[[relyingpartyregistrationresolver-entityid]]
|
||||
==== Resolving Based on the `<saml2:Response#Issuer>`
|
||||
|
||||
When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
|
||||
|
||||
This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
|
||||
|
||||
You can instead resolve the `registrationId` via the `Issuer`.
|
||||
A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
private final InMemoryRelyingPartyRegistrationRepository registrations;
|
||||
|
||||
// ... constructor
|
||||
|
||||
@Override
|
||||
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId != null) {
|
||||
return this.registrations.findByRegistrationId(registrationId);
|
||||
}
|
||||
String entityId = resolveEntityIdFromSamlResponse(request);
|
||||
for (RelyingPartyRegistration registration : this.registrations) {
|
||||
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
|
||||
RelyingPartyRegistrationResolver {
|
||||
@Override
|
||||
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
|
||||
if (registrationId != null) {
|
||||
return this.registrations.findByRegistrationId(registrationId)
|
||||
}
|
||||
String entityId = resolveEntityIdFromSamlResponse(request)
|
||||
for (val registration : this.registrations) {
|
||||
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
|
||||
return registration
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
|
||||
|
||||
[[federating-saml2-login]]
|
||||
=== Federating Login
|
||||
|
||||
One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
|
||||
In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
|
||||
|
||||
These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
||||
.stream().map((builder) -> builder
|
||||
.registrationId(UUID.randomUUID().toString())
|
||||
.entityId("https://example.org/saml2/sp")
|
||||
.build()
|
||||
)
|
||||
.collect(Collectors.toList()));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
||||
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
|
||||
.registrationId(UUID.randomUUID().toString())
|
||||
.entityId("https://example.org/saml2/sp")
|
||||
.build()
|
||||
}
|
||||
.collect(Collectors.toList()));
|
||||
----
|
||||
====
|
||||
|
||||
Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
|
||||
There are several ways to address this; let's focus on a way that suits the specific use case of federation.
|
||||
|
||||
In many federation cases, all the asserting parties share service provider configuration.
|
||||
Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`.
|
||||
|
||||
There are two main URIs you will want to change along those lines:
|
||||
|
||||
* <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
|
||||
* <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
|
||||
|
||||
[NOTE]
|
||||
Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
|
||||
|
||||
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
||||
|
||||
[[using-spring-security-saml-extension-uris]]
|
||||
=== Using Spring Security SAML Extension URIs
|
||||
|
||||
In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
|
||||
|
||||
For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
[[servlet-saml2login-metadata]]
|
||||
= Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
= Saml 2.0 Metadata
|
||||
|
||||
Spring Security can <<parsing-asserting-party-metadata,parse asserting party metadata>> to produce an `AssertingPartyDetails` instance as well as <<publishing-relying-party-metadata,publish relying party metadata>> from a `RelyingPartyRegistration` instance.
|
||||
|
||||
[[parsing-asserting-party-metadata]]
|
||||
== Parsing `<saml2:IDPSSODescriptor>` metadata
|
||||
|
||||
You can parse an asserting party's metadata xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[using `RelyingPartyRegistrations`].
|
||||
|
||||
When using the OpenSAML vendor support, the resulting `AssertingPartyDetails` will be of type `OpenSamlAssertingPartyDetails`.
|
||||
This means you'll be able to do get the underlying OpenSAML XMLObject by doing the following:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails)
|
||||
registration.getAssertingPartyDetails();
|
||||
EntityDescriptor openSamlEntityDescriptor = details.getEntityDescriptor();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val details: OpenSamlAssertingPartyDetails =
|
||||
registration.getAssertingPartyDetails() as OpenSamlAssertingPartyDetails;
|
||||
val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor();
|
||||
----
|
||||
====
|
||||
|
||||
[[publishing-relying-party-metadata]]
|
||||
== Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
|
||||
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
||||
|
||||
@@ -72,3 +103,45 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
||||
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
|
||||
----
|
||||
====
|
||||
|
||||
== Changing the Way a `RelyingPartyRegistration` Is Looked Up
|
||||
|
||||
To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
RelyingPartyRegistrationResolver myRegistrationResolver = ...;
|
||||
Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver());
|
||||
|
||||
// ...
|
||||
|
||||
http.addFilterBefore(metadata, BasicAuthenticationFilter.class);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
----
|
||||
val myRegistrationResolver: RelyingPartyRegistrationResolver = ...;
|
||||
val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver());
|
||||
|
||||
// ...
|
||||
|
||||
http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java);
|
||||
----
|
||||
====
|
||||
|
||||
In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
metadata.setRequestMatcher("/saml2/metadata")
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
----
|
||||
metadata.setRequestMatcher("/saml2/metadata")
|
||||
----
|
||||
====
|
||||
|
||||
@@ -61,6 +61,7 @@ public class WithMockUserTests {
|
||||
@ContextConfiguration
|
||||
class WithMockUserTests {
|
||||
----
|
||||
====
|
||||
|
||||
This is a basic example of how to setup Spring Security Test. The highlights are:
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): String? {
|
||||
----
|
||||
====
|
||||
|
||||
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2User` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
|
||||
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2Login` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
|
||||
@@ -1,78 +1,25 @@
|
||||
plugins {
|
||||
id 'org.antora' version '1.0.0-alpha.3'
|
||||
id 'org.antora' version '1.0.0'
|
||||
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
antora {
|
||||
version = '~3.1'
|
||||
playbook = file('local-antora-playbook.yml')
|
||||
options = ['--clean', '--stacktrace']
|
||||
environment = [
|
||||
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
|
||||
'ALGOLIA_APP_ID': '244V8V9FGG',
|
||||
'ALGOLIA_INDEX_NAME': 'security-docs'
|
||||
]
|
||||
dependencies = [
|
||||
'@antora/collector-extension': '1.0.0-alpha.2'
|
||||
]
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-security'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
}
|
||||
|
||||
tasks.register('generateAntora') {
|
||||
group = 'Documentation'
|
||||
description = 'Generates the antora.yml for dynamic properties'
|
||||
doLast {
|
||||
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/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
def ymlVersions = resolvedVersions(project.configurations.testRuntimeClasspath).call()
|
||||
.collect(v -> " ${v.getKey()}: ${v.getValue()}")
|
||||
.join('\n')
|
||||
def outputFile = layout.buildDirectory.file('generateAntora/antora.yml').get().asFile
|
||||
mkdir(outputFile.getParentFile())
|
||||
def mainVersion = project.version
|
||||
def prerelease = null
|
||||
def versionComponents = mainVersion.split(/(?=-)/)
|
||||
if (versionComponents.length > 1) {
|
||||
if (versionComponents[1] == '-SNAPSHOT') {
|
||||
mainVersion = versionComponents[0]
|
||||
prerelease = "'-SNAPSHOT'"
|
||||
} else {
|
||||
prerelease = 'true'
|
||||
}
|
||||
}
|
||||
def antoraYmlText = file('antora.yml').text
|
||||
layout.buildDirectory.file('.antora.yml').get().asFile.text = antoraYmlText
|
||||
antoraYmlText = antoraYmlText.lines().collect { l ->
|
||||
if (l.startsWith('version: ')) {
|
||||
return prerelease == null ? "version: '${mainVersion}'" : "version: '${mainVersion}'\nprerelease: ${prerelease}"
|
||||
}
|
||||
if (l.startsWith('title: ')) return "title: ${project.parent.description}"
|
||||
return l == 'ext:' || l.getAt(0) == ' ' ? null : l
|
||||
}.findAll(Objects::nonNull).join('\n')
|
||||
outputFile.text = """$antoraYmlText
|
||||
asciidoc:
|
||||
attributes:
|
||||
icondir: icons
|
||||
gh-old-samples-url: $ghOldSamplesUrl
|
||||
gh-samples-url: $ghSamplesUrl
|
||||
gh-url: $ghUrl
|
||||
security-api-url: $securityApiUrl
|
||||
security-reference-url: $securityReferenceUrl
|
||||
spring-framework-api-url: $springFrameworkApiUrl
|
||||
spring-framework-reference-url: $springFrameworkReferenceUrl
|
||||
spring-security-version: ${project.version}
|
||||
${ymlVersions}
|
||||
"""
|
||||
}
|
||||
tasks.named("generateAntoraYml") {
|
||||
asciidocAttributes = project.provider( { generateAttributes() } )
|
||||
asciidocAttributes.putAll(providers.provider( { resolvedVersions(project.configurations.testRuntimeClasspath) }))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -82,12 +29,33 @@ dependencies {
|
||||
testImplementation 'org.springframework:spring-core'
|
||||
}
|
||||
|
||||
def generateAttributes() {
|
||||
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/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
|
||||
return ['gh-old-samples-url': ghOldSamplesUrl.toString(),
|
||||
'gh-samples-url': ghSamplesUrl.toString(),
|
||||
'gh-url': ghUrl.toString(),
|
||||
'security-api-url': securityApiUrl.toString(),
|
||||
'security-reference-url': securityReferenceUrl.toString(),
|
||||
'spring-framework-api-url': springFrameworkApiUrl.toString(),
|
||||
'spring-framework-reference-url': springFrameworkReferenceUrl.toString(),
|
||||
'spring-security-version': project.version]
|
||||
+ resolvedVersions(project.configurations.testRuntimeClasspath)
|
||||
}
|
||||
|
||||
def resolvedVersions(Configuration configuration) {
|
||||
return {
|
||||
configuration.resolvedConfiguration
|
||||
return configuration.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.collectEntries { [(it.name + '-version'): it.moduleVersion.id.version] }
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
^http://www.w3.org/2000/09/xmldsig.*
|
||||
^http://www.w3.org/2001/10/xml-exc-c14n
|
||||
^http://www.w3.org/2001/04/xmldsig-more
|
||||
^http://www.w3.org/2001/04/xmlenc
|
||||
^http://www.w3.org/2001/04/xmlenc
|
||||
^http://www.springframework.org/schema/security/.*
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
aspectjVersion=1.9.9.1
|
||||
aspectjVersion=1.9.19
|
||||
springJavaformatVersion=0.0.31
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.23
|
||||
springFrameworkVersion=5.3.27
|
||||
openSamlVersion=3.4.6
|
||||
version=5.7.6-SNAPSHOT
|
||||
version=5.7.8
|
||||
kotlinVersion=1.6.21
|
||||
samplesBranch=5.7.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.userdetails;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
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.DirContextAdapter;
|
||||
import org.springframework.ldap.core.DistinguishedName;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
|
||||
import org.springframework.security.ldap.server.ApacheDSContainer;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dayan Kodippily
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(
|
||||
classes = DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests.ApacheDsContainerWithUndefinedGroupRoleAttributeConfig.class)
|
||||
public class DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests {
|
||||
|
||||
@Autowired
|
||||
private DefaultSpringSecurityContextSource contextSource;
|
||||
|
||||
private DefaultLdapAuthoritiesPopulator populator;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.populator = new DefaultLdapAuthoritiesPopulator(this.contextSource, "ou=groups");
|
||||
this.populator.setIgnorePartialResultException(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupSearchDoesNotAllowNullRoles() {
|
||||
this.populator.setRolePrefix("ROLE_");
|
||||
this.populator.setGroupRoleAttribute("ou");
|
||||
this.populator.setSearchSubtree(true);
|
||||
this.populator.setSearchSubtree(false);
|
||||
this.populator.setConvertToUpperCase(true);
|
||||
this.populator.setGroupSearchFilter("(member={0})");
|
||||
|
||||
DirContextAdapter ctx = new DirContextAdapter(
|
||||
new DistinguishedName("uid=dayan,ou=people,dc=springframework,dc=org"));
|
||||
|
||||
Set<String> authorities = AuthorityUtils.authorityListToSet(this.populator.getGrantedAuthorities(ctx, "dayan"));
|
||||
|
||||
assertThat(authorities).as("Should have 1 role").hasSize(2);
|
||||
|
||||
assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue();
|
||||
assertThat(authorities.contains("ROLE_")).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ApacheDsContainerWithUndefinedGroupRoleAttributeConfig implements DisposableBean {
|
||||
|
||||
private ApacheDSContainer container;
|
||||
|
||||
@Bean
|
||||
ApacheDSContainer ldapContainer() throws Exception {
|
||||
this.container = new ApacheDSContainer("dc=springframework,dc=org",
|
||||
"classpath:test-server-with-undefined-group-role-attributes.ldif");
|
||||
this.container.setPort(0);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ContextSource contextSource(ApacheDSContainer ldapContainer) {
|
||||
return new DefaultSpringSecurityContextSource(
|
||||
"ldap://127.0.0.1:" + ldapContainer.getLocalPort() + "/dc=springframework,dc=org");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
this.container.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
dn: ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn: ou=people,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
dn: uid=dayan,ou=people,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: person
|
||||
objectclass: organizationalPerson
|
||||
objectclass: inetOrgPerson
|
||||
cn: Dayan K
|
||||
sn: Dayan
|
||||
uid: dayan
|
||||
userPassword: dayanspassword
|
||||
|
||||
|
||||
|
||||
dn: cn=managers,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: managers
|
||||
ou:
|
||||
member: uid=dayan,ou=people,dc=springframework,dc=org
|
||||
|
||||
dn: cn=researchers,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: researchers
|
||||
member: uid=dayan,ou=people,dc=springframework,dc=org
|
||||
|
||||
dn: cn=developers,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: developers
|
||||
ou: developer
|
||||
member: uid=dayan,ou=people,dc=springframework,dc=org
|
||||
+13
-2
@@ -37,6 +37,7 @@ import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* The default strategy for obtaining user role information from the directory.
|
||||
@@ -169,7 +170,14 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
logger.info("Will perform group search from the context source base since groupSearchBase is empty.");
|
||||
}
|
||||
this.authorityMapper = (record) -> {
|
||||
String role = record.get(this.groupRoleAttribute).get(0);
|
||||
List<String> roles = record.get(this.groupRoleAttribute);
|
||||
if (CollectionUtils.isEmpty(roles)) {
|
||||
return null;
|
||||
}
|
||||
String role = roles.get(0);
|
||||
if (role == null) {
|
||||
return null;
|
||||
}
|
||||
if (this.convertToUpperCase) {
|
||||
role = role.toUpperCase();
|
||||
}
|
||||
@@ -225,7 +233,10 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
new String[] { this.groupRoleAttribute });
|
||||
logger.debug(LogMessage.of(() -> "Found roles from search " + userRoles));
|
||||
for (Map<String, List<String>> role : userRoles) {
|
||||
authorities.add(this.authorityMapper.apply(role));
|
||||
GrantedAuthority authority = this.authorityMapper.apply(role);
|
||||
if (authority != null) {
|
||||
authorities.add(authority);
|
||||
}
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
|
||||
+1
-1
@@ -192,8 +192,8 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
|
||||
if (authorizationRequest == null) {
|
||||
throw authzEx;
|
||||
}
|
||||
this.sendRedirectForAuthorization(request, response, authorizationRequest);
|
||||
this.requestCache.saveRequest(request, response);
|
||||
this.sendRedirectForAuthorization(request, response, authorizationRequest);
|
||||
}
|
||||
catch (Exception failed) {
|
||||
this.unsuccessfulRedirectForAuthorization(request, response, failed);
|
||||
|
||||
+19
@@ -48,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -333,4 +334,22 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
+ "login_hint=user@provider\\.com");
|
||||
}
|
||||
|
||||
// gh-11602
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenSaveRequestBeforeCommitted()
|
||||
throws Exception {
|
||||
String requestUri = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
willAnswer((invocation) -> assertThat((invocation.<HttpServletResponse>getArgument(1)).isCommitted()).isFalse())
|
||||
.given(this.requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
willThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId())).given(filterChain)
|
||||
.doFilter(any(ServletRequest.class), any(ServletResponse.class));
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
assertThat(response.isCommitted()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -55,6 +55,8 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor;
|
||||
import com.nimbusds.jwt.proc.JWTProcessor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -388,15 +390,19 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
});
|
||||
ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri);
|
||||
source.setWebClient(this.webClient);
|
||||
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = getExpectedJwsAlgorithms(jwsKeySelector);
|
||||
Mono<ConfigurableJWTProcessor<JWKSecurityContext>> jwtProcessorMono = this.jwtProcessorCustomizer
|
||||
Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = this.jwtProcessorCustomizer
|
||||
.apply(source, jwtProcessor)
|
||||
.map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
|
||||
.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
|
||||
return (jwt) -> {
|
||||
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
|
||||
return jwtProcessorMono.flatMap((processor) -> source.get(selector)
|
||||
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
|
||||
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList))));
|
||||
return jwtProcessorMono.flatMap((tuple) -> {
|
||||
JWTProcessor<JWKSecurityContext> processor = tuple.getT1();
|
||||
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = tuple.getT2();
|
||||
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
|
||||
return source.get(selector)
|
||||
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
|
||||
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList)));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+12
-17
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* A {@link JwtDecoder} that lazily initializes another {@link JwtDecoder}
|
||||
*
|
||||
@@ -26,12 +28,17 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public final class SupplierJwtDecoder implements JwtDecoder {
|
||||
|
||||
private final Supplier<JwtDecoder> jwtDecoderSupplier;
|
||||
|
||||
private volatile JwtDecoder delegate;
|
||||
private final Supplier<JwtDecoder> delegate;
|
||||
|
||||
public SupplierJwtDecoder(Supplier<JwtDecoder> jwtDecoderSupplier) {
|
||||
this.jwtDecoderSupplier = jwtDecoderSupplier;
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
try {
|
||||
return jwtDecoderSupplier.get();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw wrapException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,19 +46,7 @@ public final class SupplierJwtDecoder implements JwtDecoder {
|
||||
*/
|
||||
@Override
|
||||
public Jwt decode(String token) throws JwtException {
|
||||
if (this.delegate == null) {
|
||||
synchronized (this.jwtDecoderSupplier) {
|
||||
if (this.delegate == null) {
|
||||
try {
|
||||
this.delegate = this.jwtDecoderSupplier.get();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw wrapException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.delegate.decode(token);
|
||||
return this.delegate.get().decode(token);
|
||||
}
|
||||
|
||||
private JwtDecoderInitializationException wrapException(Exception ex) {
|
||||
|
||||
+2
@@ -60,6 +60,7 @@ import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.cache.support.SimpleValueWrapper;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -700,6 +701,7 @@ public class NimbusJwtDecoderTests {
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
Cache cache = mock(Cache.class);
|
||||
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
|
||||
given(cache.get(eq(JWK_SET_URI))).willReturn(new SimpleValueWrapper(JWK_SET));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK));
|
||||
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -39,6 +39,8 @@ import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
|
||||
import com.nimbusds.jose.proc.JWKSecurityContext;
|
||||
@@ -365,6 +367,20 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJwkSetUriWhenJwtProcessorCustomizerSetsJWSKeySelectorThenUseCustomizedJWSKeySelector()
|
||||
throws InvalidKeySpecException {
|
||||
WebClient webClient = mockJwkSetResponse(new JWKSet(new RSAKey.Builder(key()).build()).toString());
|
||||
// @formatter:off
|
||||
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithm(SignatureAlgorithm.ES256).webClient(webClient)
|
||||
.jwtProcessorCustomizer((p) -> p
|
||||
.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS512, new JWKSecurityContextJWKSet())))
|
||||
.build();
|
||||
assertThat(decoder.decode(this.rsa512).block()).extracting(Jwt::getSubject).isEqualTo("test-subject");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withPublicKeyWhenNullThenThrowsException() {
|
||||
// @formatter:off
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -71,7 +71,12 @@ class OpenSamlMetadataAssertingPartyDetailsConverter {
|
||||
if (xmlObject instanceof EntitiesDescriptor) {
|
||||
EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject;
|
||||
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
|
||||
builders.add(convert(descriptor));
|
||||
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
|
||||
builders.add(convert(descriptor));
|
||||
}
|
||||
}
|
||||
if (builders.isEmpty()) {
|
||||
throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements");
|
||||
}
|
||||
return builders;
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.saml2.provider.service.registration;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
class OpenSamlMetadataRelyingPartyRegistrationConverter {
|
||||
|
||||
private final OpenSamlMetadataAssertingPartyDetailsConverter converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
|
||||
|
||||
Collection<RelyingPartyRegistration.Builder> convert(InputStream source) {
|
||||
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
|
||||
for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) {
|
||||
builders.add(new RelyingPartyRegistration.Builder(builder));
|
||||
}
|
||||
return builders;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -89,8 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
|
||||
@Override
|
||||
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||
return RelyingPartyRegistration
|
||||
.withAssertingPartyDetails(this.converter.convert(inputMessage.getBody()).iterator().next().build());
|
||||
return new RelyingPartyRegistration.Builder(this.converter.convert(inputMessage.getBody()).iterator().next());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+25
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,6 +30,7 @@ import java.util.function.Function;
|
||||
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -970,6 +971,14 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
private Builder(AssertingPartyDetails.Builder assertingPartyDetailsBuilder) {
|
||||
this.assertingPartyDetailsBuilder = assertingPartyDetailsBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the asserting party's <a href=
|
||||
* "https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor">EntityID</a>.
|
||||
@@ -1032,7 +1041,7 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String registrationId;
|
||||
private Converter<ProviderDetails, String> registrationId = ProviderDetails::getEntityId;
|
||||
|
||||
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
|
||||
|
||||
@@ -1052,12 +1061,17 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private String nameIdFormat = null;
|
||||
|
||||
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
|
||||
private ProviderDetails.Builder providerDetails;
|
||||
|
||||
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new LinkedHashSet<>();
|
||||
|
||||
private Builder(String registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
this.registrationId = (party) -> registrationId;
|
||||
this.providerDetails = new ProviderDetails.Builder();
|
||||
}
|
||||
|
||||
Builder(AssertingPartyDetails.Builder builder) {
|
||||
this.providerDetails = new ProviderDetails.Builder(builder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1066,7 +1080,7 @@ public final class RelyingPartyRegistration {
|
||||
* @return this object
|
||||
*/
|
||||
public Builder registrationId(String id) {
|
||||
this.registrationId = id;
|
||||
this.registrationId = (party) -> id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1363,11 +1377,12 @@ public final class RelyingPartyRegistration {
|
||||
if (this.singleLogoutServiceResponseLocation == null) {
|
||||
this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation;
|
||||
}
|
||||
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
||||
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBinding, this.providerDetails.build(), this.nameIdFormat, this.credentials,
|
||||
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
ProviderDetails party = this.providerDetails.build();
|
||||
String registrationId = this.registrationId.convert(party);
|
||||
return new RelyingPartyRegistration(registrationId, this.entityId, this.assertionConsumerServiceLocation,
|
||||
this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation,
|
||||
this.singleLogoutServiceResponseLocation, this.singleLogoutServiceBinding, party, this.nameIdFormat,
|
||||
this.credentials, this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,13 +18,11 @@ package org.springframework.security.saml2.provider.service.registration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails;
|
||||
|
||||
/**
|
||||
* A utility class for constructing instances of {@link RelyingPartyRegistration}
|
||||
@@ -36,7 +34,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
*/
|
||||
public final class RelyingPartyRegistrations {
|
||||
|
||||
private static final OpenSamlMetadataAssertingPartyDetailsConverter assertingPartyMetadataConverter = new OpenSamlMetadataAssertingPartyDetailsConverter();
|
||||
private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
|
||||
|
||||
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
@@ -215,11 +213,7 @@ public final class RelyingPartyRegistrations {
|
||||
* @since 5.7
|
||||
*/
|
||||
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
|
||||
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
|
||||
for (AssertingPartyDetails.Builder builder : assertingPartyMetadataConverter.convert(source)) {
|
||||
builders.add(RelyingPartyRegistration.withAssertingPartyDetails(builder.build()));
|
||||
}
|
||||
return builders;
|
||||
return relyingPartyRegistrationConverter.convert(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,10 +31,12 @@ import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.NameIDPolicy;
|
||||
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
@@ -72,6 +74,8 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
|
||||
private final NameIDBuilder nameIdBuilder;
|
||||
|
||||
private final NameIDPolicyBuilder nameIdPolicyBuilder;
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSamlAuthenticationRequestResolver} using the provided
|
||||
* parameters
|
||||
@@ -92,6 +96,9 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML");
|
||||
this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML");
|
||||
this.nameIdPolicyBuilder = (NameIDPolicyBuilder) registry.getBuilderFactory()
|
||||
.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.nameIdPolicyBuilder, "nameIdPolicyBuilder must be configured in OpenSAML");
|
||||
}
|
||||
|
||||
<T extends AbstractSaml2AuthenticationRequest> T resolve(HttpServletRequest request) {
|
||||
@@ -119,6 +126,11 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
authnRequest.setIssuer(iss);
|
||||
authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
|
||||
authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation());
|
||||
if (registration.getNameIdFormat() != null) {
|
||||
NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject();
|
||||
nameIdPolicy.setFormat(registration.getNameIdFormat());
|
||||
authnRequest.setNameIDPolicy(nameIdPolicy);
|
||||
}
|
||||
authnRequestConsumer.accept(registration, authnRequest);
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
@@ -659,7 +660,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
attributeMap.addAll(attribute.getName(), attributeValues);
|
||||
}
|
||||
}
|
||||
return attributeMap;
|
||||
return new LinkedHashMap<>(attributeMap); // gh-11785
|
||||
}
|
||||
|
||||
private static List<String> getSessionIndexes(Assertion assertion) {
|
||||
|
||||
+19
@@ -32,6 +32,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
@@ -68,6 +69,7 @@ import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
@@ -349,6 +351,23 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
assertThat(principal.getSessionIndexes()).contains("session-index");
|
||||
}
|
||||
|
||||
// gh-11785
|
||||
@Test
|
||||
public void deserializeWhenAssertionContainsAttributesThenWorks() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ClassLoader loader = getClass().getClassLoader();
|
||||
mapper.registerModules(SecurityJackson2Modules.getModules(loader));
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
|
||||
assertion.getAttributeStatements().addAll(attributes);
|
||||
response.getAssertions().add(signed(assertion));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
String result = mapper.writeValueAsString(authentication);
|
||||
mapper.readValue(result, Authentication.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAssertionContainsCustomAttributesThenItSucceeds() {
|
||||
Response response = response();
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.saml2.provider.service.registration;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
|
||||
|
||||
private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
|
||||
|
||||
private String metadata;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
ClassPathResource resource = new ClassPathResource("test-metadata.xml");
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
|
||||
this.metadata = reader.lines().collect(Collectors.joining());
|
||||
}
|
||||
}
|
||||
|
||||
// gh-12667
|
||||
@Test
|
||||
public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception {
|
||||
try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) {
|
||||
this.converter.convert(source)
|
||||
.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails())
|
||||
.isInstanceOf(OpenSamlAssertingPartyDetails.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+14
@@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -243,4 +244,17 @@ public class RelyingPartyRegistrationsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionFromMetadataLocationCanHandleFederationMetadata() {
|
||||
Collection<RelyingPartyRegistration.Builder> federationMetadataWithSkippedSPEntries = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("classpath:test-federated-metadata.xml");
|
||||
assertThat(federationMetadataWithSkippedSPEntries.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionFromMetadataLocationWithoutIdpThenSaml2Exception() {
|
||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("classpath:test-metadata-without-idp.xml"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -38,7 +38,7 @@ public final class TestRelyingPartyRegistrations {
|
||||
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
||||
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId).nameIdFormat("format")
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
|
||||
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -52,6 +52,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -75,6 +76,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -110,6 +112,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -132,6 +135,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="federation_root"
|
||||
cacheDuration="P0Y0M0DT0H15M0.000S" validUntil="2099-03-04T20:18:29.383Z">
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
||||
entityID="https://localhost/simplesaml/saml2/idp/metadata.php">
|
||||
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="https://localhost/simplesaml/saml2/idp/SingleLogoutService.php"/>
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
|
||||
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="https://localhost/simplesaml/saml2/idp/SSOService.php"/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
<md:EntityDescriptor entityID="https://service.provider.org">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<idpdisco:DiscoveryResponse xmlns:idpdisco="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://nomp.se/saml/login?disco=true" index="0"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SSO" index="0" isDefault="true"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sp.provider.org/saml/SSO" index="1"/>
|
||||
<md:AttributeConsumingService index="0">
|
||||
<md:ServiceName xml:lang="en">The SP</md:ServiceName>
|
||||
<md:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42"/>
|
||||
<md:RequestedAttribute FriendlyName="surName" Name="urn:oid:2.5.4.4"/>
|
||||
</md:AttributeConsumingService>
|
||||
</md:SPSSODescriptor>
|
||||
<md:Organization>
|
||||
<md:OrganizationName xml:lang="en">Service Provider</md:OrganizationName>
|
||||
<md:OrganizationDisplayName xml:lang="en">Service Provider</md:OrganizationDisplayName>
|
||||
</md:Organization>
|
||||
</md:EntityDescriptor>
|
||||
</md:EntitiesDescriptor>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="federation_root"
|
||||
cacheDuration="P0Y0M0DT0H15M0.000S" validUntil="2099-03-04T20:18:29.383Z">
|
||||
<md:EntityDescriptor entityID="https://service.provider.org">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<idpdisco:DiscoveryResponse xmlns:idpdisco="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://nomp.se/saml/login?disco=true" index="0"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SSO" index="0" isDefault="true"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sp.provider.org/saml/SSO" index="1"/>
|
||||
<md:AttributeConsumingService index="0">
|
||||
<md:ServiceName xml:lang="en">The SP</md:ServiceName>
|
||||
<md:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42"/>
|
||||
<md:RequestedAttribute FriendlyName="surName" Name="urn:oid:2.5.4.4"/>
|
||||
</md:AttributeConsumingService>
|
||||
</md:SPSSODescriptor>
|
||||
<md:Organization>
|
||||
<md:OrganizationName xml:lang="en">Service Provider</md:OrganizationName>
|
||||
<md:OrganizationDisplayName xml:lang="en">Service Provider</md:OrganizationDisplayName>
|
||||
</md:Organization>
|
||||
</md:EntityDescriptor>
|
||||
</md:EntitiesDescriptor>
|
||||
@@ -26,6 +26,7 @@ import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -257,7 +258,11 @@ public class FilterInvocation {
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(String name) {
|
||||
return Collections.enumeration(this.headers.get(name));
|
||||
List<String> headerList = this.headers.get(name);
|
||||
if (headerList == null) {
|
||||
return Collections.emptyEnumeration();
|
||||
}
|
||||
return Collections.enumeration(headerList);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+16
@@ -27,6 +27,8 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -50,6 +52,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
|
||||
private boolean clearAuthentication = true;
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
/**
|
||||
* Requires the request to be passed in.
|
||||
* @param request from which to obtain a HTTP session (cannot be null)
|
||||
@@ -73,6 +77,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
if (this.clearAuthentication) {
|
||||
context.setAuthentication(null);
|
||||
}
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
|
||||
this.securityContextRepository.saveContext(emptyContext, request, response);
|
||||
}
|
||||
|
||||
public boolean isInvalidateHttpSession() {
|
||||
@@ -100,4 +106,14 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
this.clearAuthentication = clearAuthentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to use. Default is
|
||||
* {@link HttpSessionSecurityContextRepository}.
|
||||
* @param securityContextRepository the {@link SecurityContextRepository} to use.
|
||||
*/
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
@@ -58,6 +58,9 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -142,6 +145,8 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
|
||||
private AuthenticationFailureHandler failureHandler;
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
|
||||
@@ -179,6 +184,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
context.setAuthentication(targetUser);
|
||||
SecurityContextHolder.setContext(context);
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", targetUser));
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
// redirect to target url
|
||||
this.successHandler.onAuthenticationSuccess(request, response, targetUser);
|
||||
}
|
||||
@@ -196,6 +202,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
context.setAuthentication(originalUser);
|
||||
SecurityContextHolder.setContext(context);
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", originalUser));
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
// redirect to target url
|
||||
this.successHandler.onAuthenticationSuccess(request, response, originalUser);
|
||||
return;
|
||||
@@ -510,6 +517,19 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
this.switchAuthorityRole = switchAuthorityRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
|
||||
* switch user success. The default is
|
||||
* {@link RequestAttributeSecurityContextRepository}.
|
||||
* @param securityContextRepository the {@link SecurityContextRepository} to use.
|
||||
* Cannot be null.
|
||||
* @since 5.7.7
|
||||
*/
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
private static RequestMatcher createMatcher(String pattern) {
|
||||
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
|
||||
}
|
||||
|
||||
+38
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -137,13 +137,46 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
|
||||
SaveContextOnUpdateOrErrorResponseWrapper.class);
|
||||
if (responseWrapper == null) {
|
||||
boolean httpSessionExists = request.getSession(false) != null;
|
||||
SecurityContext initialContext = SecurityContextHolder.createEmptyContext();
|
||||
responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
|
||||
saveContextInHttpSession(context, request);
|
||||
return;
|
||||
}
|
||||
responseWrapper.saveContext(context);
|
||||
}
|
||||
|
||||
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
|
||||
if (isTransient(context) || isTransient(context.getAuthentication())) {
|
||||
return;
|
||||
}
|
||||
SecurityContext emptyContext = generateNewContext();
|
||||
if (emptyContext.equals(context)) {
|
||||
HttpSession session = request.getSession(false);
|
||||
removeContextFromSession(context, session);
|
||||
}
|
||||
else {
|
||||
boolean createSession = this.allowSessionCreation;
|
||||
HttpSession session = request.getSession(createSession);
|
||||
setContextInSession(context, session);
|
||||
}
|
||||
}
|
||||
|
||||
private void setContextInSession(SecurityContext context, HttpSession session) {
|
||||
if (session != null) {
|
||||
session.setAttribute(this.springSecurityContextKey, context);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeContextFromSession(SecurityContext context, HttpSession session) {
|
||||
if (session != null) {
|
||||
session.removeAttribute(this.springSecurityContextKey);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsContext(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
@@ -369,11 +402,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
// We may have a new session, so check also whether the context attribute
|
||||
// is set SEC-1561
|
||||
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
|
||||
httpSession.setAttribute(springSecurityContextKey, context);
|
||||
HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request);
|
||||
this.isSaveContextInvoked = true;
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -142,4 +145,23 @@ public class FilterInvocationTests {
|
||||
assertThat(filterInvocation.getRequest().getServletContext()).isSameAs(mockServletContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDummyRequestGetHeaders() {
|
||||
DummyRequest request = new DummyRequest();
|
||||
request.addHeader("known", "val");
|
||||
Enumeration<String> headers = request.getHeaders("known");
|
||||
assertThat(headers.hasMoreElements()).isTrue();
|
||||
assertThat(headers.nextElement()).isEqualTo("val");
|
||||
assertThat(headers.hasMoreElements()).isFalse();
|
||||
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDummyRequestGetHeadersNull() {
|
||||
DummyRequest request = new DummyRequest();
|
||||
Enumeration<String> headers = request.getHeaders("unknown");
|
||||
assertThat(headers.hasMoreElements()).isFalse();
|
||||
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 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,11 +16,16 @@
|
||||
|
||||
package org.springframework.security.web.authentication;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -108,4 +113,20 @@ public class SimpleUrlAuthenticationSuccessHandlerTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ash.setTargetUrlParameter(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveAuthenticationAttributeWhenOnAuthenticationSuccess() throws Exception {
|
||||
SimpleUrlAuthenticationSuccessHandler ash = new SimpleUrlAuthenticationSuccessHandler();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession();
|
||||
assertThat(session).isNotNull();
|
||||
session.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
|
||||
new BadCredentialsException("Invalid credentials"));
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNotNull();
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION))
|
||||
.isInstanceOf(AuthenticationException.class);
|
||||
ash.onAuthenticationSuccess(request, response, mock(Authentication.class));
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,12 +27,19 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
public class SecurityContextLogoutHandlerTests {
|
||||
|
||||
@@ -76,4 +83,35 @@ public class SecurityContextLogoutHandlerTests {
|
||||
assertThat(beforeContext.getAuthentication()).isSameAs(beforeAuthentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenSecurityContextRepositoryThenSaveEmptyContext() {
|
||||
SecurityContextRepository repository = mock(SecurityContextRepository.class);
|
||||
this.handler.setSecurityContextRepository(repository);
|
||||
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
|
||||
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenClearAuthenticationFalseThenSaveEmptyContext() {
|
||||
SecurityContextRepository repository = mock(SecurityContextRepository.class);
|
||||
this.handler.setSecurityContextRepository(repository);
|
||||
this.handler.setClearAuthentication(false);
|
||||
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
|
||||
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenDefaultSecurityContextRepositoryThenHttpSessionSecurityContextRepository() {
|
||||
SecurityContextRepository securityContextRepository = (SecurityContextRepository) ReflectionTestUtils
|
||||
.getField(this.handler, "securityContextRepository");
|
||||
assertThat(securityContextRepository).isInstanceOf(HttpSessionSecurityContextRepository.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSecurityContextRepositoryWhenNullThenException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.handler.setSecurityContextRepository(null))
|
||||
.withMessage("securityContextRepository cannot be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+60
@@ -16,15 +16,18 @@
|
||||
|
||||
package org.springframework.security.web.authentication.switchuser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
@@ -44,11 +47,15 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.util.FieldUtils;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -483,6 +490,59 @@ public class SwitchUserFilterTests {
|
||||
filter.setSwitchFailureUrl("/foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenDefaultSecurityContextRepositoryThenHttpSessionRepository() {
|
||||
SwitchUserFilter switchUserFilter = new SwitchUserFilter();
|
||||
assertThat(ReflectionTestUtils.getField(switchUserFilter, "securityContextRepository"))
|
||||
.isInstanceOf(HttpSessionSecurityContextRepository.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenSwitchUserThenSaveSecurityContext() throws ServletException, IOException {
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain filterChain = new MockFilterChain();
|
||||
request.setParameter(SwitchUserFilter.SPRING_SECURITY_SWITCH_USERNAME_KEY, "jacklord");
|
||||
request.setRequestURI("/login/impersonate");
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
filter.setSecurityContextRepository(securityContextRepository);
|
||||
filter.setUserDetailsService(new MockUserDetailsService());
|
||||
filter.setTargetUrl("/target");
|
||||
filter.afterPropertiesSet();
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(securityContextRepository).saveContext(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenExitUserThenSaveSecurityContext() throws ServletException, IOException {
|
||||
UsernamePasswordAuthenticationToken source = UsernamePasswordAuthenticationToken.authenticated("dano",
|
||||
"hawaii50", ROLES_12);
|
||||
// set current user (Admin)
|
||||
List<GrantedAuthority> adminAuths = new ArrayList<>(ROLES_12);
|
||||
adminAuths.add(new SwitchUserGrantedAuthority("PREVIOUS_ADMINISTRATOR", source));
|
||||
UsernamePasswordAuthenticationToken admin = UsernamePasswordAuthenticationToken.authenticated("jacklord",
|
||||
"hawaii50", adminAuths);
|
||||
SecurityContextHolder.getContext().setAuthentication(admin);
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain filterChain = new MockFilterChain();
|
||||
request.setParameter(SwitchUserFilter.SPRING_SECURITY_SWITCH_USERNAME_KEY, "jacklord");
|
||||
request.setRequestURI("/logout/impersonate");
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
filter.setSecurityContextRepository(securityContextRepository);
|
||||
filter.setUserDetailsService(new MockUserDetailsService());
|
||||
filter.setTargetUrl("/target");
|
||||
filter.afterPropertiesSet();
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(securityContextRepository).saveContext(any(), any(), any());
|
||||
}
|
||||
|
||||
private class MockUserDetailsService implements UserDetailsService {
|
||||
|
||||
private String password = "hawaii50";
|
||||
|
||||
+49
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -53,6 +53,7 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.core.context.TransientSecurityContext;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
@@ -763,6 +764,53 @@ public class HttpSessionSecurityContextRepositoryTests {
|
||||
assertThat(session).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextEmptyThenRemoveAttributeFromSession() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
|
||||
MockHttpSession session = (MockHttpSession) request.getSession(true);
|
||||
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, emptyContext);
|
||||
repo.saveContext(emptyContext, request, response);
|
||||
Object attributeAfterSave = session
|
||||
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
assertThat(attributeAfterSave).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextEmptyAndNoSessionThenDoesNotCreateSession() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
|
||||
repo.saveContext(emptyContext, request, response);
|
||||
assertThat(request.getSession(false)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextThenSaveInSession() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext context = createSecurityContext(PasswordEncodedUser.user());
|
||||
repo.saveContext(context, request, response);
|
||||
Object savedContext = request.getSession()
|
||||
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
assertThat(savedContext).isEqualTo(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenTransientAuthenticationThenDoNotSave() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(new SomeTransientAuthentication());
|
||||
repo.saveContext(context, request, response);
|
||||
assertThat(request.getSession(false)).isNull();
|
||||
}
|
||||
|
||||
private SecurityContext createSecurityContext(UserDetails userDetails) {
|
||||
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails,
|
||||
userDetails.getPassword(), userDetails.getAuthorities());
|
||||
|
||||
Reference in New Issue
Block a user