mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-30 08:42:13 +00:00
Revert unnecessary commits from main
Issue gh-15016
This commit is contained in:
parent
e4745c99e9
commit
08f11f06ab
41
.github/dependabot.template.yml
vendored
41
.github/dependabot.template.yml
vendored
@ -1,41 +0,0 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
registries:
|
||||
- "spring-milestones"
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
# GitHub Actions
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: "main"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: "sjohnr/*"
|
||||
- dependency-name: "spring-io/*"
|
||||
- dependency-name: "spring-security-release-tools/*"
|
219
.github/dependabot.yml
vendored
219
.github/dependabot.yml
vendored
@ -1,161 +1,72 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
updates:
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 5.8.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.1.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.2.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: com.gradle.enterprise
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 5.8.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 6.1.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 6.2.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: docs-build
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
|
||||
- package-ecosystem: npm
|
||||
target-branch: docs-build
|
||||
directory: /
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
milestone: 319 # 6.2.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
registries:
|
||||
- "spring-milestones"
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "6.1.x"
|
||||
milestone: 318 # 6.1.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "5.8.x"
|
||||
milestone: 246 # 5.8.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "io.mockk:mockk" # mockk updates break tests
|
||||
- dependency-name: "org.opensaml:*" # org.opensaml maintains two different versions, so it must be updated manually
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
@ -1,57 +0,0 @@
|
||||
name: Auto Merge Forward Dependabot Commits
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: dependabot-auto-merge-forward
|
||||
|
||||
jobs:
|
||||
get-supported-branches:
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
|
||||
with:
|
||||
project: spring-security
|
||||
type: oss
|
||||
repository_name: spring-projects/spring-security
|
||||
|
||||
auto-merge-forward-dependabot:
|
||||
name: Auto Merge Forward Dependabot Commits
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-supported-branches]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Setup GitHub User
|
||||
id: setup-gh-user
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Run Auto Merge Forward
|
||||
id: run-auto-merge-forward
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/auto-merge-forward@actions-v1
|
||||
with:
|
||||
branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
|
||||
from-author: dependabot[bot]
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [ auto-merge-forward-dependabot ]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
2
.github/workflows/release-scheduler.yml
vendored
2
.github/workflows/release-scheduler.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# List of active maintenance branches.
|
||||
branch: [ main, 6.2.x, 6.1.x, 5.8.x ]
|
||||
branch: [ main, 6.1.x, 5.8.x ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
36
.github/workflows/update-dependabot.yml
vendored
36
.github/workflows/update-dependabot.yml
vendored
@ -1,36 +0,0 @@
|
||||
name: Update dependabot.yml
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
get-supported-branches:
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
|
||||
with:
|
||||
project: spring-security
|
||||
type: oss
|
||||
repository_name: spring-projects/spring-security
|
||||
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-supported-branches]
|
||||
if: ${{ (github.repository == 'spring-projects/spring-security') && (github.ref == 'refs/heads/main') }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: spring-io/spring-security-release-tools/.github/actions/generate-dependabot-yml@actions-v1
|
||||
name: Update dependabot.yml
|
||||
with:
|
||||
gradle-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
|
||||
github-actions-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main,docs-build
|
||||
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Update dependabot.yml
|
@ -105,7 +105,7 @@ Once merged, the fix will be forwarded-ported to applicable branches including `
|
||||
1. Create a local branch
|
||||
If this is for an issue, consider a branch name with the issue number, like `gh-22276`.
|
||||
[[write-tests]]
|
||||
1. Add documentation and JUnit Tests for your changes.
|
||||
1. Add JUnit Tests for your changes
|
||||
[[update-copyright]]
|
||||
1. In all files you edited, if the copyright header is of the form 2002-20xx, update the final copyright year to the current year.
|
||||
[[add-since]]
|
||||
|
@ -1,8 +1,8 @@
|
||||
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
|
||||
|
||||
image:https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml"]
|
||||
image:https://github.com/spring-projects/spring-security/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions?query=workflow%3ACI"]
|
||||
|
||||
image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
|
||||
image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
|
||||
|
||||
= Spring Security
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -103,13 +103,6 @@ public class PostAuthorizeAspectTests {
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedDenyAllPostAuthorizeDeniesAccess() {
|
||||
SecurityContextHolder.getContext().setAuthentication(this.anne);
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
|
||||
}
|
||||
|
||||
interface SecuredInterface {
|
||||
|
||||
@PostAuthorize("hasRole('X')")
|
||||
@ -141,10 +134,6 @@ public class PostAuthorizeAspectTests {
|
||||
privateMethod();
|
||||
}
|
||||
|
||||
NestedObject myObject() {
|
||||
return new NestedObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class SecuredImplSubclass extends SecuredImpl {
|
||||
@ -168,13 +157,4 @@ public class PostAuthorizeAspectTests {
|
||||
|
||||
}
|
||||
|
||||
static class NestedObject {
|
||||
|
||||
@PostAuthorize("denyAll")
|
||||
void denyAllMethod() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -54,11 +54,6 @@ public class PostFilterAspectTests {
|
||||
assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedDenyAllPostFilterDeniesAccess() {
|
||||
assertThat(this.prePostSecured.myObject().denyAllMethod()).isEmpty();
|
||||
}
|
||||
|
||||
static class PrePostSecured {
|
||||
|
||||
@PostFilter("filterObject.startsWith('a')")
|
||||
@ -66,19 +61,6 @@ public class PostFilterAspectTests {
|
||||
return objects;
|
||||
}
|
||||
|
||||
NestedObject myObject() {
|
||||
return new NestedObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NestedObject {
|
||||
|
||||
@PostFilter("filterObject == null")
|
||||
List<String> denyAllMethod() {
|
||||
return new ArrayList<>(List.of("deny"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -103,13 +103,6 @@ public class PreAuthorizeAspectTests {
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedDenyAllPreAuthorizeDeniesAccess() {
|
||||
SecurityContextHolder.getContext().setAuthentication(this.anne);
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
|
||||
}
|
||||
|
||||
interface SecuredInterface {
|
||||
|
||||
@PreAuthorize("hasRole('X')")
|
||||
@ -141,10 +134,6 @@ public class PreAuthorizeAspectTests {
|
||||
privateMethod();
|
||||
}
|
||||
|
||||
NestedObject myObject() {
|
||||
return new NestedObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class SecuredImplSubclass extends SecuredImpl {
|
||||
@ -168,13 +157,4 @@ public class PreAuthorizeAspectTests {
|
||||
|
||||
}
|
||||
|
||||
static class NestedObject {
|
||||
|
||||
@PreAuthorize("denyAll")
|
||||
void denyAllMethod() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -54,11 +54,6 @@ public class PreFilterAspectTests {
|
||||
assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedDenyAllPreFilterDeniesAccess() {
|
||||
assertThat(this.prePostSecured.myObject().denyAllMethod(new ArrayList<>(List.of("deny")))).isEmpty();
|
||||
}
|
||||
|
||||
static class PrePostSecured {
|
||||
|
||||
@PreFilter("filterObject.startsWith('a')")
|
||||
@ -66,19 +61,6 @@ public class PreFilterAspectTests {
|
||||
return objects;
|
||||
}
|
||||
|
||||
NestedObject myObject() {
|
||||
return new NestedObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NestedObject {
|
||||
|
||||
@PreFilter("filterObject == null")
|
||||
List<String> denyAllMethod(List<String> list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -28,7 +28,6 @@ import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@ -62,8 +61,6 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
|
||||
*/
|
||||
private boolean encodeServiceUrlWithSessionId = true;
|
||||
|
||||
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.hasLength(this.loginUrl, "loginUrl must be specified");
|
||||
@ -77,7 +74,8 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
|
||||
String urlEncodedService = createServiceUrl(servletRequest, response);
|
||||
String redirectUrl = createRedirectUrl(urlEncodedService);
|
||||
preCommence(servletRequest, response);
|
||||
this.redirectStrategy.sendRedirect(servletRequest, response, redirectUrl);
|
||||
new DefaultRedirectStrategy().sendRedirect(servletRequest, response, redirectUrl);
|
||||
// response.sendRedirect(redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,14 +149,4 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
|
||||
return this.encodeServiceUrlWithSessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RedirectStrategy} to use
|
||||
* @param redirectStrategy the {@link RedirectStrategy} to use
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
|
||||
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
|
||||
this.redirectStrategy = redirectStrategy;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
|
||||
import org.apereo.cas.client.util.WebUtils;
|
||||
import org.apereo.cas.client.validation.TicketValidator;
|
||||
@ -41,20 +40,14 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
|
||||
@ -207,10 +200,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
public CasAuthenticationFilter() {
|
||||
@ -251,22 +240,7 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
return null;
|
||||
}
|
||||
String serviceTicket = obtainArtifact(request);
|
||||
if (!StringUtils.hasText(serviceTicket)) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null && session
|
||||
.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR) != null) {
|
||||
this.logger.debug("Failed authentication response from CAS gateway request");
|
||||
session.removeAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR);
|
||||
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||
if (savedRequest != null) {
|
||||
String redirectUrl = savedRequest.getRedirectUrl();
|
||||
this.logger.debug(LogMessage.format("Redirecting to: %s", redirectUrl));
|
||||
this.requestCache.removeRequest(request, response);
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceTicket == null) {
|
||||
this.logger.debug("Failed to obtain an artifact (cas ticket)");
|
||||
serviceTicket = "";
|
||||
}
|
||||
@ -344,28 +318,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link RedirectStrategy} used to redirect to the saved request if there is
|
||||
* one saved. Defaults to {@link DefaultRedirectStrategy}.
|
||||
* @param redirectStrategy the redirect strategy to use
|
||||
* @since 6.3
|
||||
*/
|
||||
public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
|
||||
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
|
||||
this.redirectStrategy = redirectStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link RequestCache} used to retrieve the saved request in failed gateway
|
||||
* authentication scenarios.
|
||||
* @param requestCache the request cache to use
|
||||
* @since 6.3
|
||||
*/
|
||||
public final void setRequestCache(RequestCache requestCache) {
|
||||
Assert.notNull(requestCache, "requestCache cannot be null");
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the request is elgible to process a service ticket. This method exists
|
||||
* for readability.
|
||||
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* 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.cas.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apereo.cas.client.util.CommonUtils;
|
||||
import org.apereo.cas.client.util.WebUtils;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
/**
|
||||
* Redirects the request to the CAS server appending {@code gateway=true} to the URL. Upon
|
||||
* redirection, the {@link ServiceProperties#isSendRenew()} is ignored and considered as
|
||||
* {@code false} to align with the specification says that the {@code sendRenew} parameter
|
||||
* is not compatible with the {@code gateway} parameter. See the <a href=
|
||||
* "https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html#:~:text=This%20parameter%20is%20not%20compatible%20with%20the%20%E2%80%9Crenew%E2%80%9D%20parameter.%20Behavior%20is%20undefined%20if%20both%20are%20set.">CAS
|
||||
* Protocol Specification</a> for more details. To allow other filters to know if the
|
||||
* request is a gateway request, this filter creates a session and add an attribute with
|
||||
* name {@link #CAS_GATEWAY_AUTHENTICATION_ATTR} which can be checked by other filters if
|
||||
* needed. It is recommended that this filter is placed after
|
||||
* {@link CasAuthenticationFilter} if it is defined.
|
||||
*
|
||||
* @author Michael Remond
|
||||
* @author Jerome LELEU
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class CasGatewayAuthenticationRedirectFilter extends GenericFilterBean {
|
||||
|
||||
public static final String CAS_GATEWAY_AUTHENTICATION_ATTR = "CAS_GATEWAY_AUTHENTICATION";
|
||||
|
||||
private final String casLoginUrl;
|
||||
|
||||
private final ServiceProperties serviceProperties;
|
||||
|
||||
private RequestMatcher requestMatcher;
|
||||
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
/**
|
||||
* Constructs a new instance of this class
|
||||
* @param serviceProperties the {@link ServiceProperties}
|
||||
*/
|
||||
public CasGatewayAuthenticationRedirectFilter(String casLoginUrl, ServiceProperties serviceProperties) {
|
||||
Assert.hasText(casLoginUrl, "casLoginUrl cannot be null or empty");
|
||||
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
|
||||
this.casLoginUrl = casLoginUrl;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.requestMatcher = new CasGatewayResolverRequestMatcher(this.serviceProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) res;
|
||||
|
||||
if (!this.requestMatcher.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestCache.saveRequest(request, response);
|
||||
HttpSession session = request.getSession(true);
|
||||
session.setAttribute(CAS_GATEWAY_AUTHENTICATION_ATTR, true);
|
||||
String urlEncodedService = WebUtils.constructServiceUrl(request, response, this.serviceProperties.getService(),
|
||||
null, this.serviceProperties.getServiceParameter(), this.serviceProperties.getArtifactParameter(),
|
||||
true);
|
||||
String redirectUrl = CommonUtils.constructRedirectUrl(this.casLoginUrl,
|
||||
this.serviceProperties.getServiceParameter(), urlEncodedService, false, true);
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RequestMatcher} used to trigger this filter. Defaults to
|
||||
* {@link CasGatewayResolverRequestMatcher}.
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
public void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
|
||||
this.requestMatcher = requestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RequestCache} used to store the current request to be replayed
|
||||
* after redirect from the CAS server. Defaults to {@link HttpSessionRequestCache}.
|
||||
* @param requestCache the {@link RequestCache} to use
|
||||
*/
|
||||
public void setRequestCache(RequestCache requestCache) {
|
||||
Assert.notNull(requestCache, "requestCache cannot be null");
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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.cas.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
|
||||
import org.apereo.cas.client.authentication.GatewayResolver;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link RequestMatcher} implementation that delegates the check to an instance of
|
||||
* {@link GatewayResolver}. The request is marked as "gatewayed" using the configured
|
||||
* {@link GatewayResolver} to avoid infinite loop.
|
||||
*
|
||||
* @author Michael Remond
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class CasGatewayResolverRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final ServiceProperties serviceProperties;
|
||||
|
||||
private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
|
||||
|
||||
public CasGatewayResolverRequestMatcher(ServiceProperties serviceProperties) {
|
||||
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
|
||||
this.serviceProperties = serviceProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, this.serviceProperties.getService());
|
||||
if (!wasGatewayed) {
|
||||
this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link GatewayResolver} to check if the request was already gatewayed.
|
||||
* Defaults to {@link DefaultGatewayResolverImpl}
|
||||
* @param gatewayStorage the {@link GatewayResolver} to use. Cannot be null.
|
||||
*/
|
||||
public void setGatewayStorage(GatewayResolver gatewayStorage) {
|
||||
Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
|
||||
this.gatewayStorage = gatewayStorage;
|
||||
}
|
||||
|
||||
}
|
@ -16,22 +16,16 @@
|
||||
|
||||
package org.springframework.security.cas.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
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.cas.ServiceProperties;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests {@link CasAuthenticationEntryPoint}.
|
||||
@ -101,25 +95,4 @@ public class CasAuthenticationEntryPointTests {
|
||||
.isEqualTo(response.getRedirectedUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setRedirectStrategyThenUses() throws IOException {
|
||||
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint();
|
||||
ServiceProperties sp = new ServiceProperties();
|
||||
|
||||
sp.setService("https://mycompany.com/login/cas");
|
||||
ep.setServiceProperties(sp);
|
||||
ep.setLoginUrl("https://cas/login");
|
||||
|
||||
RedirectStrategy redirectStrategy = mock();
|
||||
|
||||
ep.setRedirectStrategy(redirectStrategy);
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
|
||||
ep.commence(req, res, new BadCredentialsException("bad credentials"));
|
||||
|
||||
verify(redirectStrategy).sendRedirect(eq(req), eq(res),
|
||||
eq("https://cas/login?service=https%3A%2F%2Fmycompany.com%2Flogin%2Fcas"));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -42,7 +41,6 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ -226,25 +224,6 @@ public class CasAuthenticationFilterTests {
|
||||
verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attemptAuthenticationWhenNoServiceTicketAndIsGatewayRequestThenRedirectToSavedRequestAndClearAttribute()
|
||||
throws Exception {
|
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession(true);
|
||||
session.setAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR, true);
|
||||
|
||||
new HttpSessionRequestCache().saveRequest(request, response);
|
||||
|
||||
Authentication authn = filter.attemptAuthentication(request, response);
|
||||
assertThat(authn).isNull();
|
||||
assertThat(response.getStatus()).isEqualTo(302);
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue");
|
||||
assertThat(session.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulAuthenticationWhenSecurityContextRepositorySetThenUses() throws ServletException, IOException {
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* 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.cas.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link CasGatewayAuthenticationRedirectFilter}.
|
||||
*
|
||||
* @author Jerome LELEU
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
public class CasGatewayAuthenticationRedirectFilterTests {
|
||||
|
||||
private static final String CAS_LOGIN_URL = "http://mycasserver/login";
|
||||
|
||||
CasGatewayAuthenticationRedirectFilter filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL,
|
||||
serviceProperties());
|
||||
|
||||
@Test
|
||||
void doFilterWhenMatchesThenSavesRequestAndSavesAttributeAndSendRedirect() throws IOException, ServletException {
|
||||
RequestCache requestCache = mock();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter.setRequestMatcher((req) -> true);
|
||||
this.filter.setRequestCache(requestCache);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
assertThat(response.getHeader("Location"))
|
||||
.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
|
||||
verify(requestCache).saveRequest(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNotMatchThenContinueFilter() throws ServletException, IOException {
|
||||
this.filter.setRequestMatcher((req) -> false);
|
||||
FilterChain chain = mock();
|
||||
MockHttpServletResponse response = mock();
|
||||
this.filter.doFilter(new MockHttpServletRequest(), response, chain);
|
||||
verify(chain).doFilter(any(), any());
|
||||
verifyNoInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenSendRenewTrueThenIgnores() throws ServletException, IOException {
|
||||
ServiceProperties serviceProperties = serviceProperties();
|
||||
serviceProperties.setSendRenew(true);
|
||||
this.filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL, serviceProperties);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter.setRequestMatcher((req) -> true);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
assertThat(response.getHeader("Location"))
|
||||
.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
|
||||
}
|
||||
|
||||
private static ServiceProperties serviceProperties() {
|
||||
ServiceProperties serviceProperties = new ServiceProperties();
|
||||
serviceProperties.setService("http://localhost/login/cas");
|
||||
return serviceProperties;
|
||||
}
|
||||
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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.cas.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests {@link CasGatewayResolverRequestMatcher}.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class CasGatewayResolverRequestMatcherTests {
|
||||
|
||||
CasGatewayResolverRequestMatcher matcher = new CasGatewayResolverRequestMatcher(new ServiceProperties());
|
||||
|
||||
@Test
|
||||
void constructorWhenServicePropertiesNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CasGatewayResolverRequestMatcher(null))
|
||||
.withMessage("serviceProperties cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenAlreadyGatewayedThenReturnsFalse() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.getSession().setAttribute("_const_cas_gateway_", "yes");
|
||||
boolean matches = this.matcher.matches(request);
|
||||
assertThat(matches).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenNotGatewayedThenReturnsTrue() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
boolean matches = this.matcher.matches(request);
|
||||
assertThat(matches).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenNoSessionThenReturnsTrue() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setSession(null);
|
||||
boolean matches = this.matcher.matches(request);
|
||||
assertThat(matches).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenNotGatewayedAndCheckedAgainThenSavesAsGatewayedAndReturnsFalse() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
boolean matches = this.matcher.matches(request);
|
||||
boolean secondMatch = this.matcher.matches(request);
|
||||
assertThat(matches).isTrue();
|
||||
assertThat(secondMatch).isFalse();
|
||||
}
|
||||
|
||||
}
|
@ -96,7 +96,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
|
||||
pc.getReaderContext()
|
||||
.fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or "
|
||||
+ "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema "
|
||||
+ "with Spring Security 6.3. Please update your schema declarations to the 6.3 schema.",
|
||||
+ "with Spring Security 6.2. Please update your schema declarations to the 6.2 schema.",
|
||||
element);
|
||||
}
|
||||
String name = pc.getDelegate().getLocalName(element);
|
||||
@ -221,7 +221,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
|
||||
|
||||
private boolean matchesVersionInternal(Element element) {
|
||||
String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation");
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.3.*.xsd.*")
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.2.*.xsd.*")
|
||||
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|
||||
|| !schemaLocation.matches("(?m).*spring-security.*");
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,7 +20,6 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
@ -66,21 +65,14 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
}
|
||||
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
|
||||
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
|
||||
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class);
|
||||
DaoAuthenticationProvider provider;
|
||||
if (passwordEncoder != null) {
|
||||
provider = new DaoAuthenticationProvider(passwordEncoder);
|
||||
}
|
||||
else {
|
||||
provider = new DaoAuthenticationProvider();
|
||||
}
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
if (passwordEncoder != null) {
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
}
|
||||
if (passwordManager != null) {
|
||||
provider.setUserDetailsPasswordService(passwordManager);
|
||||
}
|
||||
if (passwordChecker != null) {
|
||||
provider.setCompromisedPasswordChecker(passwordChecker);
|
||||
}
|
||||
provider.afterPropertiesSet();
|
||||
auth.authenticationProvider(provider);
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
|
||||
import org.springframework.security.config.Customizer;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider,
|
||||
ObjectProvider<Customizer<AuthorizationAdvisorProxyFactory>> customizers) {
|
||||
List<AuthorizationAdvisor> advisors = new ArrayList<>();
|
||||
provider.forEach(advisors::add);
|
||||
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults();
|
||||
customizers.forEach((c) -> c.customize(factory));
|
||||
factory.setAdvisors(advisors);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
|
||||
AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
|
||||
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
|
||||
authorizationProxyFactory);
|
||||
List<AuthorizationAdvisor> advisors = new ArrayList<>();
|
||||
provider.forEach(advisors::add);
|
||||
advisors.add(interceptor);
|
||||
authorizationProxyFactory.setAdvisors(advisors);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
@ -19,26 +19,18 @@ package org.springframework.security.config.annotation.method.configuration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ObservationAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
final class DeferringObservationAuthorizationManager<T>
|
||||
implements AuthorizationManager<T>, MethodAuthorizationDeniedHandler {
|
||||
final class DeferringObservationAuthorizationManager<T> implements AuthorizationManager<T> {
|
||||
|
||||
private final Supplier<AuthorizationManager<T>> delegate;
|
||||
|
||||
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
|
||||
|
||||
DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
|
||||
AuthorizationManager<T> delegate) {
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
@ -48,9 +40,6 @@ final class DeferringObservationAuthorizationManager<T>
|
||||
}
|
||||
return new ObservationAuthorizationManager<>(registry, delegate);
|
||||
});
|
||||
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
|
||||
this.handler = h;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -58,15 +47,4 @@ final class DeferringObservationAuthorizationManager<T>
|
||||
return this.delegate.get().check(authentication, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
||||
return this.handler.handleDeniedInvocation(methodInvocation, authorizationResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return this.handler.handleDeniedInvocationResult(methodInvocationResult, authorizationResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,27 +19,19 @@ package org.springframework.security.config.annotation.method.configuration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
final class DeferringObservationReactiveAuthorizationManager<T>
|
||||
implements ReactiveAuthorizationManager<T>, MethodAuthorizationDeniedHandler {
|
||||
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
|
||||
|
||||
private final Supplier<ReactiveAuthorizationManager<T>> delegate;
|
||||
|
||||
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
|
||||
|
||||
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
|
||||
ReactiveAuthorizationManager<T> delegate) {
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
@ -49,9 +41,6 @@ final class DeferringObservationReactiveAuthorizationManager<T>
|
||||
}
|
||||
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
|
||||
});
|
||||
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
|
||||
this.handler = h;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -59,15 +48,4 @@ final class DeferringObservationReactiveAuthorizationManager<T>
|
||||
return this.delegate.get().check(authentication, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
||||
return this.handler.handleDeniedInvocation(methodInvocation, authorizationResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return this.handler.handleDeniedInvocationResult(methodInvocationResult, authorizationResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -87,14 +87,4 @@ public @interface EnableMethodSecurity {
|
||||
*/
|
||||
AdviceMode mode() default AdviceMode.PROXY;
|
||||
|
||||
/**
|
||||
* Indicate additional offset in the ordering of the execution of the security
|
||||
* interceptors when multiple advices are applied at a specific joinpoint. I.e.,
|
||||
* precedence of each security interceptor enabled by this annotation will be
|
||||
* calculated as sum of its default precedence and offset. The default is 0.
|
||||
* @return the offset in the order the security advisor should be applied
|
||||
* @since 6.3
|
||||
*/
|
||||
int offset() default 0;
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -20,18 +20,11 @@ import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
|
||||
@ -49,23 +42,15 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
final class Jsr250MethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor jsr250AuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
Jsr250MethodSecurityConfiguration configuration) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
Jsr250AuthorizationManager jsr250 = new Jsr250AuthorizationManager();
|
||||
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
|
||||
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
|
||||
authoritiesAuthorizationManager.setRoleHierarchy(roleHierarchy);
|
||||
jsr250.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager);
|
||||
defaultsProvider.ifAvailable((d) -> jsr250.setRolePrefix(d.getRolePrefix()));
|
||||
SecurityContextHolderStrategy strategy = strategyProvider
|
||||
.getIfAvailable(SecurityContextHolder::getContextHolderStrategy);
|
||||
@ -73,16 +58,8 @@ final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrast
|
||||
registryProvider, jsr250);
|
||||
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
|
||||
.jsr250(manager);
|
||||
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
|
||||
interceptor.setSecurityContextHolderStrategy(strategy);
|
||||
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ class MethodSecurityAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
registerAsAdvisor("postAuthorizeAuthorization", registry);
|
||||
registerAsAdvisor("securedAuthorization", registry);
|
||||
registerAsAdvisor("jsr250Authorization", registry);
|
||||
registerAsAdvisor("authorizeReturnObject", registry);
|
||||
}
|
||||
|
||||
private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -56,7 +56,6 @@ final class MethodSecuritySelector implements ImportSelector {
|
||||
if (annotation.jsr250Enabled()) {
|
||||
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
|
||||
}
|
||||
imports.add(AuthorizationProxyConfiguration.class.getName());
|
||||
return imports.toArray(new String[0]);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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,41 +16,33 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
@ -64,102 +56,73 @@ import org.springframework.util.function.SingletonSupplier;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
final class PrePostMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor preFilterAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
|
||||
ApplicationContext context) {
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
|
||||
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
|
||||
preFilter.setOrder(preFilter.getOrder() + configuration.interceptorOrderOffset);
|
||||
return new DeferringMethodInterceptor<>(preFilter, (f) -> {
|
||||
methodSecurityDefaultsProvider.ifAvailable(f::setTemplateDefaults);
|
||||
f.setExpressionHandler(expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
|
||||
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
|
||||
});
|
||||
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
|
||||
preFilter.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
return preFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
|
||||
manager.setApplicationContext(context);
|
||||
manager.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
|
||||
.preAuthorize(manager(manager, registryProvider));
|
||||
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
||||
return new DeferringMethodInterceptor<>(preAuthorize, (f) -> {
|
||||
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
|
||||
manager.setExpressionHandler(expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
|
||||
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
|
||||
eventPublisherProvider.ifAvailable(f::setAuthorizationEventPublisher);
|
||||
});
|
||||
strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
|
||||
eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
|
||||
return preAuthorize;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
|
||||
manager.setApplicationContext(context);
|
||||
manager.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
|
||||
.postAuthorize(manager(manager, registryProvider));
|
||||
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
||||
return new DeferringMethodInterceptor<>(postAuthorize, (f) -> {
|
||||
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
|
||||
manager.setExpressionHandler(expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
|
||||
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
|
||||
eventPublisherProvider.ifAvailable(f::setAuthorizationEventPublisher);
|
||||
});
|
||||
strategyProvider.ifAvailable(postAuthorize::setSecurityContextHolderStrategy);
|
||||
eventPublisherProvider.ifAvailable(postAuthorize::setAuthorizationEventPublisher);
|
||||
return postAuthorize;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor postFilterAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
|
||||
ApplicationContext context) {
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
|
||||
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
|
||||
postFilter.setOrder(postFilter.getOrder() + configuration.interceptorOrderOffset);
|
||||
return new DeferringMethodInterceptor<>(postFilter, (f) -> {
|
||||
methodSecurityDefaultsProvider.ifAvailable(f::setTemplateDefaults);
|
||||
f.setExpressionHandler(expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
|
||||
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
|
||||
});
|
||||
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
|
||||
postFilter.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
return postFilter;
|
||||
}
|
||||
|
||||
private static MethodSecurityExpressionHandler defaultExpressionHandler(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, ApplicationContext context) {
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext context) {
|
||||
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
||||
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
|
||||
handler.setRoleHierarchy(roleHierarchy);
|
||||
defaultsProvider.ifAvailable((d) -> handler.setDefaultRolePrefix(d.getRolePrefix()));
|
||||
handler.setApplicationContext(context);
|
||||
return handler;
|
||||
@ -170,54 +133,41 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfras
|
||||
return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
|
||||
|
||||
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
|
||||
implements AuthorizationAdvisor {
|
||||
private final Supplier<MethodSecurityExpressionHandler> expressionHandler;
|
||||
|
||||
private final Pointcut pointcut;
|
||||
|
||||
private final int order;
|
||||
|
||||
private final Supplier<M> delegate;
|
||||
|
||||
DeferringMethodInterceptor(M delegate, Consumer<M> supplier) {
|
||||
this.pointcut = delegate.getPointcut();
|
||||
this.order = delegate.getOrder();
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
supplier.accept(delegate);
|
||||
return delegate;
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
|
||||
return this.delegate.get().invoke(invocation);
|
||||
private DeferringMethodSecurityExpressionHandler(
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext applicationContext) {
|
||||
this.expressionHandler = SingletonSupplier.of(() -> expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, applicationContext)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
return this.pointcut;
|
||||
public ExpressionParser getExpressionParser() {
|
||||
return this.expressionHandler.get().getExpressionParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this;
|
||||
public EvaluationContext createEvaluationContext(Authentication authentication, MethodInvocation invocation) {
|
||||
return this.expressionHandler.get().createEvaluationContext(authentication, invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
|
||||
MethodInvocation invocation) {
|
||||
return this.expressionHandler.get().createEvaluationContext(authentication, invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) {
|
||||
return this.expressionHandler.get().filter(filterTarget, filterExpression, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReturnObject(Object returnObject, EvaluationContext ctx) {
|
||||
this.expressionHandler.get().setReturnObject(returnObject, ctx);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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,22 +16,12 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
@ -39,7 +29,6 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
@ -47,9 +36,7 @@ import org.springframework.security.authorization.method.PostAuthorizeReactiveAu
|
||||
import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* Configuration for a {@link ReactiveAuthenticationManager} based Method Security.
|
||||
@ -58,58 +45,38 @@ import org.springframework.util.function.SingletonSupplier;
|
||||
* @since 5.8
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements AopInfrastructureBean {
|
||||
final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
|
||||
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
|
||||
PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
|
||||
expressionHandler);
|
||||
return new DeferringMethodInterceptor<>(interceptor,
|
||||
(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
|
||||
static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
|
||||
MethodSecurityExpressionHandler expressionHandler) {
|
||||
return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
|
||||
MethodSecurityExpressionHandler expressionHandler,
|
||||
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
|
||||
expressionHandler);
|
||||
manager.setApplicationContext(context);
|
||||
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
|
||||
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
|
||||
.preAuthorize(authorizationManager);
|
||||
return new DeferringMethodInterceptor<>(interceptor,
|
||||
(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
|
||||
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
|
||||
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
|
||||
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
|
||||
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
|
||||
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
|
||||
PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
|
||||
expressionHandler);
|
||||
return new DeferringMethodInterceptor<>(interceptor,
|
||||
(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
|
||||
static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
|
||||
MethodSecurityExpressionHandler expressionHandler) {
|
||||
return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
|
||||
MethodSecurityExpressionHandler expressionHandler,
|
||||
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
|
||||
expressionHandler);
|
||||
manager.setApplicationContext(context);
|
||||
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
|
||||
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
|
||||
.postAuthorize(authorizationManager);
|
||||
return new DeferringMethodInterceptor<>(interceptor,
|
||||
(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
|
||||
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
|
||||
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
|
||||
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
|
||||
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -128,50 +95,4 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements A
|
||||
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
|
||||
}
|
||||
|
||||
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
|
||||
implements AuthorizationAdvisor {
|
||||
|
||||
private final Pointcut pointcut;
|
||||
|
||||
private final int order;
|
||||
|
||||
private final Supplier<M> delegate;
|
||||
|
||||
DeferringMethodInterceptor(M delegate, Consumer<M> supplier) {
|
||||
this.pointcut = delegate.getPointcut();
|
||||
this.order = delegate.getOrder();
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
supplier.accept(delegate);
|
||||
return delegate;
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
|
||||
return this.delegate.get().invoke(invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
return this.pointcut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPerInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
|
||||
import org.springframework.security.config.Customizer;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider,
|
||||
ObjectProvider<Customizer<AuthorizationAdvisorProxyFactory>> customizers) {
|
||||
List<AuthorizationAdvisor> advisors = new ArrayList<>();
|
||||
provider.forEach(advisors::add);
|
||||
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withReactiveDefaults();
|
||||
customizers.forEach((c) -> c.customize(factory));
|
||||
factory.setAdvisors(advisors);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
|
||||
AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
|
||||
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
|
||||
authorizationProxyFactory);
|
||||
List<AuthorizationAdvisor> advisors = new ArrayList<>();
|
||||
provider.forEach(advisors::add);
|
||||
advisors.add(interceptor);
|
||||
authorizationProxyFactory.setAdvisors(advisors);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
@ -51,15 +51,13 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
|
||||
else {
|
||||
imports.add(ReactiveMethodSecurityConfiguration.class.getName());
|
||||
}
|
||||
imports.add(ReactiveAuthorizationProxyConfiguration.class.getName());
|
||||
return imports.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private static final class AutoProxyRegistrarSelector
|
||||
extends AdviceModeImportSelector<EnableReactiveMethodSecurity> {
|
||||
|
||||
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName(),
|
||||
MethodSecurityAdvisorRegistrar.class.getName() };
|
||||
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
|
||||
|
||||
@Override
|
||||
protected String[] selectImports(@NonNull AdviceMode adviceMode) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -20,19 +20,12 @@ import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.SecuredAuthorizationManager;
|
||||
@ -49,38 +42,22 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
final class SecuredMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor securedAuthorizationMethodInterceptor(
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
SecuredMethodSecurityConfiguration configuration) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
SecuredAuthorizationManager secured = new SecuredAuthorizationManager();
|
||||
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
|
||||
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
|
||||
authoritiesAuthorizationManager.setRoleHierarchy(roleHierarchy);
|
||||
secured.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager);
|
||||
SecurityContextHolderStrategy strategy = strategyProvider
|
||||
.getIfAvailable(SecurityContextHolder::getContextHolderStrategy);
|
||||
AuthorizationManager<MethodInvocation> manager = new DeferringObservationAuthorizationManager<>(
|
||||
registryProvider, secured);
|
||||
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
|
||||
.secured(manager);
|
||||
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
|
||||
interceptor.setSecurityContextHolderStrategy(strategy);
|
||||
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,13 +51,11 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
@ -185,8 +183,7 @@ final class OAuth2ClientConfiguration {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider.class,
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider.class,
|
||||
PasswordOAuth2AuthorizedClientProvider.class,
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class,
|
||||
TokenExchangeOAuth2AuthorizedClientProvider.class
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
@ -258,12 +255,6 @@ final class OAuth2ClientConfiguration {
|
||||
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
OAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (tokenExchangeAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
|
||||
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
|
||||
}
|
||||
@ -373,25 +364,6 @@ final class OAuth2ClientConfiguration {
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, TokenExchangeOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
TokenExchangeGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
@ -31,14 +30,12 @@ import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationManagers;
|
||||
import org.springframework.security.authorization.ObservationAuthorizationManager;
|
||||
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
|
||||
@ -247,14 +244,11 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
* {@link RequestMatcher}s.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class AuthorizedUrl {
|
||||
|
||||
private final List<? extends RequestMatcher> matchers;
|
||||
|
||||
private boolean not;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
* @param matchers the {@link RequestMatcher} instances to map
|
||||
@ -267,16 +261,6 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
return this.matchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates the following authorization rule.
|
||||
* @return the {@link AuthorizedUrl} for further customization
|
||||
* @since 6.3
|
||||
*/
|
||||
public AuthorizedUrl not() {
|
||||
this.not = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that URLs are allowed by anyone.
|
||||
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
|
||||
@ -389,21 +373,6 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
return access(AuthenticatedAuthorizationManager.anonymous());
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that a path variable in URL to be compared.
|
||||
*
|
||||
* <p>
|
||||
* For example, <pre>
|
||||
* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName)
|
||||
* </pre>
|
||||
* @param variable the variable in URL template to compare.
|
||||
* @return {@link AuthorizedUrlVariable} for further customization.
|
||||
* @since 6.3
|
||||
*/
|
||||
public AuthorizedUrlVariable hasVariable(String variable) {
|
||||
return new AuthorizedUrlVariable(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows specifying a custom {@link AuthorizationManager}.
|
||||
* @param manager the {@link AuthorizationManager} to use
|
||||
@ -413,44 +382,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
public AuthorizationManagerRequestMatcherRegistry access(
|
||||
AuthorizationManager<RequestAuthorizationContext> manager) {
|
||||
Assert.notNull(manager, "manager cannot be null");
|
||||
return (this.not)
|
||||
? AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, AuthorizationManagers.not(manager))
|
||||
: AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that allows configuring {@link RequestMatcher}s with URI path
|
||||
* variables
|
||||
*
|
||||
* @author Taehong Kim
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class AuthorizedUrlVariable {
|
||||
|
||||
private final String variable;
|
||||
|
||||
private AuthorizedUrlVariable(String variable) {
|
||||
this.variable = variable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the value of a path variable in the URI with an `Authentication`
|
||||
* attribute
|
||||
* <p>
|
||||
* For example, <pre>
|
||||
* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName));
|
||||
* </pre>
|
||||
* @param function a function to get value from {@link Authentication}.
|
||||
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
|
||||
* customization.
|
||||
*/
|
||||
public AuthorizationManagerRequestMatcherRegistry equalTo(Function<Authentication, String> function) {
|
||||
return access((auth, requestContext) -> {
|
||||
String value = requestContext.getVariables().get(this.variable);
|
||||
return new AuthorizationDecision(function.apply(auth.get()).equals(value));
|
||||
});
|
||||
}
|
||||
|
||||
return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -19,15 +19,17 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
|
||||
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
|
||||
return JwtValidators.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
|
||||
new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,413 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.DelegatingReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
|
||||
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||
|
||||
/**
|
||||
* {@link Configuration} for OAuth 2.0 Client support.
|
||||
*
|
||||
* <p>
|
||||
* This {@code Configuration} is conditionally imported by
|
||||
* {@link ReactiveOAuth2ClientImportSelector} when the
|
||||
* {@code spring-security-oauth2-client} module is present on the classpath.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 6.3
|
||||
* @see ReactiveOAuth2ClientImportSelector
|
||||
*/
|
||||
@Import({ ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration.class,
|
||||
ReactiveOAuth2ClientConfiguration.OAuth2ClientWebFluxSecurityConfiguration.class })
|
||||
final class ReactiveOAuth2ClientConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ReactiveOAuth2AuthorizedClientManagerConfiguration {
|
||||
|
||||
@Bean(name = ReactiveOAuth2AuthorizedClientManagerRegistrar.BEAN_NAME)
|
||||
ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() {
|
||||
return new ReactiveOAuth2AuthorizedClientManagerRegistrar();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer {
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar;
|
||||
|
||||
@Override
|
||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
|
||||
if (authorizedClientManager != null) {
|
||||
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientManager(List<ReactiveOAuth2AuthorizedClientManager> authorizedClientManager) {
|
||||
if (authorizedClientManager.size() == 1) {
|
||||
this.authorizedClientManager = authorizedClientManager.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
void setAuthorizedClientManagerRegistrar(
|
||||
ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) {
|
||||
this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
if (this.authorizedClientManager != null) {
|
||||
return this.authorizedClientManager;
|
||||
}
|
||||
return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A registrar for registering the default
|
||||
* {@link ReactiveOAuth2AuthorizedClientManager} bean definition, if not already
|
||||
* present.
|
||||
*/
|
||||
static final class ReactiveOAuth2AuthorizedClientManagerRegistrar
|
||||
implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
|
||||
|
||||
static final String BEAN_NAME = "authorizedClientManagerRegistrar";
|
||||
|
||||
static final String FACTORY_METHOD_NAME = "getAuthorizedClientManager";
|
||||
|
||||
// @formatter:off
|
||||
private static final Set<Class<?>> KNOWN_AUTHORIZED_CLIENT_PROVIDERS = Set.of(
|
||||
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class,
|
||||
RefreshTokenReactiveOAuth2AuthorizedClientProvider.class,
|
||||
ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class,
|
||||
PasswordReactiveOAuth2AuthorizedClientProvider.class,
|
||||
JwtBearerReactiveOAuth2AuthorizedClientProvider.class,
|
||||
TokenExchangeReactiveOAuth2AuthorizedClientProvider.class
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
|
||||
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
if (getBeanNamesForType(ReactiveOAuth2AuthorizedClientManager.class).length != 0
|
||||
|| getBeanNamesForType(ReactiveClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(ServerOAuth2AuthorizedClientRepository.class).length != 1
|
||||
&& getBeanNamesForType(ReactiveOAuth2AuthorizedClientService.class).length != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(ReactiveOAuth2AuthorizedClientManager.class)
|
||||
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
|
||||
.getBeanDefinition();
|
||||
|
||||
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
|
||||
beanDefinition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = (ListableBeanFactory) beanFactory;
|
||||
}
|
||||
|
||||
ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() {
|
||||
if (getBeanNamesForType(ReactiveClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(ServerOAuth2AuthorizedClientRepository.class).length != 1
|
||||
&& getBeanNamesForType(ReactiveOAuth2AuthorizedClientService.class).length != 1) {
|
||||
return null;
|
||||
}
|
||||
return getAuthorizedClientManager();
|
||||
}
|
||||
|
||||
ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, ReactiveClientRegistrationRepository.class, true, true);
|
||||
|
||||
ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
try {
|
||||
authorizedClientRepository = BeanFactoryUtils.beanOfTypeIncludingAncestors(this.beanFactory,
|
||||
ServerOAuth2AuthorizedClientRepository.class, true, true);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, ReactiveOAuth2AuthorizedClientService.class, true,
|
||||
true);
|
||||
authorizedClientRepository = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(
|
||||
authorizedClientService);
|
||||
}
|
||||
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, ReactiveOAuth2AuthorizedClientProvider.class, true,
|
||||
true)
|
||||
.values();
|
||||
|
||||
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
|
||||
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
|
||||
}
|
||||
else {
|
||||
List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
|
||||
authorizedClientProviders
|
||||
.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders
|
||||
.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
|
||||
ReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (jwtBearerAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
ReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (tokenExchangeAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
|
||||
authorizedClientProvider = new DelegatingReactiveOAuth2AuthorizedClientProvider(
|
||||
authorizedClientProviders);
|
||||
}
|
||||
|
||||
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
|
||||
Consumer<DefaultReactiveOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(Consumer.class,
|
||||
DefaultReactiveOAuth2AuthorizedClientManager.class));
|
||||
if (authorizedClientManagerConsumer != null) {
|
||||
authorizedClientManagerConsumer.accept(authorizedClientManager);
|
||||
}
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
|
||||
private boolean hasDelegatingAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
if (authorizedClientProviders.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
return authorizedClientProviders.iterator()
|
||||
.next() instanceof DelegatingReactiveOAuth2AuthorizedClientProvider;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, RefreshTokenReactiveOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
|
||||
OAuth2RefreshTokenGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
|
||||
OAuth2ClientCredentialsGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, PasswordReactiveOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
|
||||
OAuth2PasswordGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, JwtBearerReactiveOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
|
||||
JwtBearerGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, TokenExchangeReactiveOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
|
||||
TokenExchangeGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private List<ReactiveOAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
List<ReactiveOAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
|
||||
authorizedClientProviders);
|
||||
additionalAuthorizedClientProviders
|
||||
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
|
||||
return additionalAuthorizedClientProviders;
|
||||
}
|
||||
|
||||
private <T extends ReactiveOAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
|
||||
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
|
||||
T authorizedClientProvider = null;
|
||||
for (ReactiveOAuth2AuthorizedClientProvider current : authorizedClientProviders) {
|
||||
if (providerClass.isInstance(current)) {
|
||||
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
|
||||
authorizedClientProvider = providerClass.cast(current);
|
||||
}
|
||||
}
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private static void assertAuthorizedClientProviderIsNull(
|
||||
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
if (authorizedClientProvider != null) {
|
||||
// @formatter:off
|
||||
throw new BeanInitializationException(String.format(
|
||||
"Unable to create a %s bean. Expected one bean of type %s, but found multiple. " +
|
||||
"Please consider defining only a single bean of this type, or define a %s bean yourself.",
|
||||
ReactiveOAuth2AuthorizedClientManager.class.getName(),
|
||||
authorizedClientProvider.getClass().getName(),
|
||||
ReactiveOAuth2AuthorizedClientManager.class.getName()));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
private <T> String[] getBeanNamesForType(Class<T> beanClass) {
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, true, true);
|
||||
}
|
||||
|
||||
private <T> T getBeanOfType(ResolvableType resolvableType) {
|
||||
ObjectProvider<T> objectProvider = this.beanFactory.getBeanProvider(resolvableType, true);
|
||||
return objectProvider.getIfAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -16,13 +16,27 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportSelector;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
|
||||
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||
|
||||
/**
|
||||
* Used by {@link EnableWebFluxSecurity} to conditionally import
|
||||
* {@link ReactiveOAuth2ClientConfiguration}.
|
||||
* {@link Configuration} for OAuth 2.0 Client support.
|
||||
*
|
||||
* <p>
|
||||
* This {@code Configuration} is imported by {@link EnableWebFluxSecurity}
|
||||
@ -46,8 +60,85 @@ final class ReactiveOAuth2ClientImportSelector implements ImportSelector {
|
||||
if (!oauth2ClientPresent) {
|
||||
return new String[0];
|
||||
}
|
||||
return new String[] {
|
||||
"org.springframework.security.config.annotation.web.reactive.ReactiveOAuth2ClientConfiguration" };
|
||||
return new String[] { "org.springframework.security.config.annotation.web.reactive."
|
||||
+ "ReactiveOAuth2ClientImportSelector$OAuth2ClientWebFluxSecurityConfiguration" };
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer {
|
||||
|
||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
@Override
|
||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
|
||||
if (authorizedClientManager != null) {
|
||||
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setClientRegistrationRepository(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientRepository(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientService(List<ReactiveOAuth2AuthorizedClientService> authorizedClientService) {
|
||||
if (authorizedClientService.size() == 1) {
|
||||
this.authorizedClientService = authorizedClientService.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientManager(List<ReactiveOAuth2AuthorizedClientManager> authorizedClientManager) {
|
||||
if (authorizedClientManager.size() == 1) {
|
||||
this.authorizedClientManager = authorizedClientManager.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
|
||||
if (this.authorizedClientRepository != null) {
|
||||
return this.authorizedClientRepository;
|
||||
}
|
||||
if (this.authorizedClientService != null) {
|
||||
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(this.authorizedClientService);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
if (this.authorizedClientManager != null) {
|
||||
return this.authorizedClientManager;
|
||||
}
|
||||
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = null;
|
||||
if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) {
|
||||
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
|
||||
.builder()
|
||||
.authorizationCode()
|
||||
.refreshToken()
|
||||
.clientCredentials()
|
||||
.password()
|
||||
.build();
|
||||
DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, getAuthorizedClientRepository());
|
||||
defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
authorizedClientManager = defaultReactiveOAuth2AuthorizedClientManager;
|
||||
}
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.security.authentication.ObservationReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
@ -64,8 +63,6 @@ class ServerHttpSecurityConfiguration {
|
||||
|
||||
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
|
||||
|
||||
private ReactiveCompromisedPasswordChecker compromisedPasswordChecker;
|
||||
|
||||
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
|
||||
|
||||
@Autowired(required = false)
|
||||
@ -101,11 +98,6 @@ class ServerHttpSecurityConfiguration {
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setCompromisedPasswordChecker(ReactiveCompromisedPasswordChecker compromisedPasswordChecker) {
|
||||
this.compromisedPasswordChecker = compromisedPasswordChecker;
|
||||
}
|
||||
|
||||
@Bean
|
||||
static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
|
||||
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) {
|
||||
@ -161,7 +153,6 @@ class ServerHttpSecurityConfiguration {
|
||||
manager.setPasswordEncoder(this.passwordEncoder);
|
||||
}
|
||||
manager.setUserDetailsPasswordService(this.userDetailsPasswordService);
|
||||
manager.setCompromisedPasswordChecker(this.compromisedPasswordChecker);
|
||||
if (!this.observationRegistry.isNoop()) {
|
||||
return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager);
|
||||
}
|
||||
|
@ -44,13 +44,11 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
@ -78,8 +76,7 @@ final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegi
|
||||
RefreshTokenOAuth2AuthorizedClientProvider.class,
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider.class,
|
||||
PasswordOAuth2AuthorizedClientProvider.class,
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class,
|
||||
TokenExchangeOAuth2AuthorizedClientProvider.class
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
@ -140,12 +137,6 @@ final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegi
|
||||
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
OAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (tokenExchangeAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
|
||||
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
|
||||
}
|
||||
@ -254,25 +245,6 @@ final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegi
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, TokenExchangeOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
TokenExchangeGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
|
||||
|
@ -64,8 +64,6 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
|
||||
private static final String ELT_ENCRYPTION_CREDENTIAL = "encryption-credential";
|
||||
|
||||
private static final String ATT_ID = "id";
|
||||
|
||||
private static final String ATT_REGISTRATION_ID = "registration-id";
|
||||
|
||||
private static final String ATT_ASSERTING_PARTY_ID = "asserting-party-id";
|
||||
@ -110,11 +108,8 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
.rootBeanDefinition(InMemoryRelyingPartyRegistrationRepository.class)
|
||||
.addConstructorArgValue(relyingPartyRegistrations)
|
||||
.getBeanDefinition();
|
||||
String relyingPartyRegistrationRepositoryId = element.getAttribute(ATT_ID);
|
||||
if (!StringUtils.hasText(relyingPartyRegistrationRepositoryId)) {
|
||||
relyingPartyRegistrationRepositoryId = parserContext.getReaderContext()
|
||||
.generateBeanName(relyingPartyRegistrationRepositoryBean);
|
||||
}
|
||||
String relyingPartyRegistrationRepositoryId = parserContext.getReaderContext()
|
||||
.generateBeanName(relyingPartyRegistrationRepositoryBean);
|
||||
parserContext.registerBeanComponent(new BeanComponentDefinition(relyingPartyRegistrationRepositoryBean,
|
||||
relyingPartyRegistrationRepositoryId));
|
||||
parserContext.popAndRegisterContainingComponent();
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -19,15 +19,17 @@ package org.springframework.security.config.web.server;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
|
||||
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
|
||||
return JwtValidators.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
|
||||
new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -63,7 +63,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
@ -127,26 +126,19 @@ import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerHttpBasicAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.SessionLimit;
|
||||
import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
|
||||
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
|
||||
@ -216,8 +208,6 @@ import org.springframework.web.server.ServerWebExchangeDecorator;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
@ -324,8 +314,6 @@ public class ServerHttpSecurity {
|
||||
|
||||
private LoginPageSpec loginPage = new LoginPageSpec();
|
||||
|
||||
private SessionManagementSpec sessionManagement;
|
||||
|
||||
private ReactiveAuthenticationManager authenticationManager;
|
||||
|
||||
private ServerSecurityContextRepository securityContextRepository;
|
||||
@ -374,7 +362,6 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds a {@link WebFilter} before specific position.
|
||||
* @param webFilter the {@link WebFilter} to add
|
||||
* @param order the place before which to insert the {@link WebFilter}
|
||||
@ -758,36 +745,6 @@ public class ServerHttpSecurity {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures Session Management. An example configuration is provided below:
|
||||
* <pre class="code">
|
||||
* @Bean
|
||||
* SecurityWebFilterChain filterChain(ServerHttpSecurity http, ReactiveSessionRegistry sessionRegistry) {
|
||||
* http
|
||||
* // ...
|
||||
* .sessionManagement((sessionManagement) -> sessionManagement
|
||||
* .concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
* .maxSessions(1)
|
||||
* .maxSessionsPreventsLogin(true)
|
||||
* .sessionRegistry(sessionRegistry)
|
||||
* )
|
||||
* );
|
||||
* return http.build();
|
||||
* }
|
||||
* </pre>
|
||||
* @param customizer the {@link Customizer} to provide more options for the
|
||||
* {@link SessionManagementSpec}
|
||||
* @return the {@link ServerHttpSecurity} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public ServerHttpSecurity sessionManagement(Customizer<SessionManagementSpec> customizer) {
|
||||
if (this.sessionManagement == null) {
|
||||
this.sessionManagement = new SessionManagementSpec();
|
||||
}
|
||||
customizer.customize(this.sessionManagement);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures password management. An example configuration is provided below:
|
||||
*
|
||||
@ -1562,9 +1519,6 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
WebFilter securityContextRepositoryWebFilter = securityContextRepositoryWebFilter();
|
||||
this.webFilters.add(securityContextRepositoryWebFilter);
|
||||
if (this.sessionManagement != null) {
|
||||
this.sessionManagement.configure(this);
|
||||
}
|
||||
if (this.httpsRedirectSpec != null) {
|
||||
this.httpsRedirectSpec.configure(this);
|
||||
}
|
||||
@ -1955,270 +1909,6 @@ public class ServerHttpSecurity {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures how sessions are managed.
|
||||
*/
|
||||
public class SessionManagementSpec {
|
||||
|
||||
private ConcurrentSessionsSpec concurrentSessions;
|
||||
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ReactiveSessionRegistry sessionRegistry;
|
||||
|
||||
private SessionLimit sessionLimit = SessionLimit.UNLIMITED;
|
||||
|
||||
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;
|
||||
|
||||
/**
|
||||
* Configures how many sessions are allowed for a given user.
|
||||
* @param customizer the customizer to provide more options
|
||||
* @return the {@link SessionManagementSpec} to customize
|
||||
*/
|
||||
public SessionManagementSpec concurrentSessions(Customizer<ConcurrentSessionsSpec> customizer) {
|
||||
if (this.concurrentSessions == null) {
|
||||
this.concurrentSessions = new ConcurrentSessionsSpec();
|
||||
}
|
||||
customizer.customize(this.concurrentSessions);
|
||||
return this;
|
||||
}
|
||||
|
||||
void configure(ServerHttpSecurity http) {
|
||||
if (this.concurrentSessions != null) {
|
||||
ReactiveSessionRegistry reactiveSessionRegistry = getSessionRegistry();
|
||||
ConcurrentSessionControlServerAuthenticationSuccessHandler concurrentSessionControlStrategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(
|
||||
reactiveSessionRegistry, getMaximumSessionsExceededHandler());
|
||||
concurrentSessionControlStrategy.setSessionLimit(this.sessionLimit);
|
||||
RegisterSessionServerAuthenticationSuccessHandler registerSessionAuthenticationStrategy = new RegisterSessionServerAuthenticationSuccessHandler(
|
||||
reactiveSessionRegistry);
|
||||
this.authenticationSuccessHandler = new DelegatingServerAuthenticationSuccessHandler(
|
||||
concurrentSessionControlStrategy, registerSessionAuthenticationStrategy);
|
||||
SessionRegistryWebFilter sessionRegistryWebFilter = new SessionRegistryWebFilter(
|
||||
reactiveSessionRegistry);
|
||||
configureSuccessHandlerOnAuthenticationFilters();
|
||||
http.addFilterAfter(sessionRegistryWebFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
|
||||
}
|
||||
}
|
||||
|
||||
private ServerMaximumSessionsExceededHandler getMaximumSessionsExceededHandler() {
|
||||
if (this.maximumSessionsExceededHandler != null) {
|
||||
return this.maximumSessionsExceededHandler;
|
||||
}
|
||||
DefaultWebSessionManager webSessionManager = getBeanOrNull(
|
||||
WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, DefaultWebSessionManager.class);
|
||||
if (webSessionManager != null) {
|
||||
this.maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(
|
||||
webSessionManager.getSessionStore());
|
||||
}
|
||||
if (this.maximumSessionsExceededHandler == null) {
|
||||
throw new IllegalStateException(
|
||||
"Could not create a default ServerMaximumSessionsExceededHandler. Please provide "
|
||||
+ "a ServerMaximumSessionsExceededHandler via DSL");
|
||||
}
|
||||
return this.maximumSessionsExceededHandler;
|
||||
}
|
||||
|
||||
private void configureSuccessHandlerOnAuthenticationFilters() {
|
||||
if (ServerHttpSecurity.this.formLogin != null) {
|
||||
ServerHttpSecurity.this.formLogin.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
|
||||
}
|
||||
if (ServerHttpSecurity.this.oauth2Login != null) {
|
||||
ServerHttpSecurity.this.oauth2Login.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
|
||||
}
|
||||
if (ServerHttpSecurity.this.httpBasic != null) {
|
||||
ServerHttpSecurity.this.httpBasic.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private ReactiveSessionRegistry getSessionRegistry() {
|
||||
if (this.sessionRegistry == null) {
|
||||
this.sessionRegistry = getBeanOrNull(ReactiveSessionRegistry.class);
|
||||
}
|
||||
if (this.sessionRegistry == null) {
|
||||
throw new IllegalStateException(
|
||||
"A ReactiveSessionRegistry is needed for concurrent session management");
|
||||
}
|
||||
return this.sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures how many sessions are allowed for a given user.
|
||||
*/
|
||||
public class ConcurrentSessionsSpec {
|
||||
|
||||
/**
|
||||
* Sets the {@link ReactiveSessionRegistry} to use.
|
||||
* @param reactiveSessionRegistry the {@link ReactiveSessionRegistry} to use
|
||||
* @return the {@link ConcurrentSessionsSpec} to continue customizing
|
||||
*/
|
||||
public ConcurrentSessionsSpec sessionRegistry(ReactiveSessionRegistry reactiveSessionRegistry) {
|
||||
SessionManagementSpec.this.sessionRegistry = reactiveSessionRegistry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of sessions allowed for any user. You can use
|
||||
* {@link SessionLimit#of(int)} to specify a positive integer or
|
||||
* {@link SessionLimit#UNLIMITED} to allow unlimited sessions. To customize
|
||||
* the maximum number of sessions on a per-user basis, you can provide a
|
||||
* custom {@link SessionLimit} implementation, like so: <pre>
|
||||
* http
|
||||
* .sessionManagement((sessions) -> sessions
|
||||
* .concurrentSessions((concurrency) -> concurrency
|
||||
* .maximumSessions((authentication) -> {
|
||||
* if (authentication.getName().equals("admin")) {
|
||||
* return Mono.empty() // unlimited sessions for admin
|
||||
* }
|
||||
* return Mono.just(1); // one session for every other user
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
* </pre>
|
||||
* @param sessionLimit the maximum number of sessions allowed for any user
|
||||
* @return the {@link ConcurrentSessionsSpec} to continue customizing
|
||||
*/
|
||||
public ConcurrentSessionsSpec maximumSessions(SessionLimit sessionLimit) {
|
||||
Assert.notNull(sessionLimit, "sessionLimit cannot be null");
|
||||
SessionManagementSpec.this.sessionLimit = sessionLimit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ServerMaximumSessionsExceededHandler} to use when the
|
||||
* maximum number of sessions is exceeded.
|
||||
* @param maximumSessionsExceededHandler the
|
||||
* {@link ServerMaximumSessionsExceededHandler} to use
|
||||
* @return the {@link ConcurrentSessionsSpec} to continue customizing
|
||||
*/
|
||||
public ConcurrentSessionsSpec maximumSessionsExceededHandler(
|
||||
ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
|
||||
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
|
||||
SessionManagementSpec.this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SessionRegistryWebFilter implements WebFilter {
|
||||
|
||||
private final ReactiveSessionRegistry sessionRegistry;
|
||||
|
||||
private SessionRegistryWebFilter(ReactiveSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return chain.filter(new SessionRegistryWebExchange(exchange));
|
||||
}
|
||||
|
||||
private final class SessionRegistryWebExchange extends ServerWebExchangeDecorator {
|
||||
|
||||
private final Mono<WebSession> sessionMono;
|
||||
|
||||
private SessionRegistryWebExchange(ServerWebExchange delegate) {
|
||||
super(delegate);
|
||||
this.sessionMono = delegate.getSession()
|
||||
.flatMap((session) -> SessionRegistryWebFilter.this.sessionRegistry
|
||||
.updateLastAccessTime(session.getId())
|
||||
.thenReturn(session))
|
||||
.map(SessionRegistryWebSession::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> getSession() {
|
||||
return this.sessionMono;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SessionRegistryWebSession implements WebSession {
|
||||
|
||||
private final WebSession session;
|
||||
|
||||
private SessionRegistryWebSession(WebSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.session.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.session.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.session.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return this.session.isStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> changeSessionId() {
|
||||
String currentId = this.session.getId();
|
||||
return this.session.changeSessionId()
|
||||
.then(Mono.defer(
|
||||
() -> SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
|
||||
.flatMap((information) -> {
|
||||
information = information.withSessionId(this.session.getId());
|
||||
return SessionRegistryWebFilter.this.sessionRegistry
|
||||
.saveSessionInformation(information);
|
||||
})));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> invalidate() {
|
||||
String currentId = this.session.getId();
|
||||
return this.session.invalidate()
|
||||
.then(Mono.defer(() -> SessionRegistryWebFilter.this.sessionRegistry
|
||||
.removeSessionInformation(currentId)))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save() {
|
||||
return this.session.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.session.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.session.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessTime() {
|
||||
return this.session.getLastAccessTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdleTime(Duration maxIdleTime) {
|
||||
this.session.setMaxIdleTime(maxIdleTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxIdleTime() {
|
||||
return this.session.getMaxIdleTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures HTTPS redirection rules
|
||||
*
|
||||
@ -2523,11 +2213,6 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
|
||||
List.of(new WebFilterChainServerAuthenticationSuccessHandler()));
|
||||
|
||||
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
|
||||
|
||||
private HttpBasicSpec() {
|
||||
List<DelegateEntry> entryPoints = new ArrayList<>();
|
||||
entryPoints
|
||||
@ -2538,40 +2223,6 @@ public class ServerHttpSecurity {
|
||||
this.entryPoint = defaultEntryPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link WebFilterChainServerAuthenticationSuccessHandler}.
|
||||
* Note that this method clears previously added success handlers via
|
||||
* {@link #authenticationSuccessHandler(Consumer)}
|
||||
* @param authenticationSuccessHandler the success handler to use
|
||||
* @return the {@link HttpBasicSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public HttpBasicSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
authenticationSuccessHandler((handlers) -> {
|
||||
handlers.clear();
|
||||
handlers.add(authenticationSuccessHandler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
|
||||
* default list contains a
|
||||
* {@link WebFilterChainServerAuthenticationSuccessHandler}.
|
||||
* @param handlersConsumer the handlers consumer
|
||||
* @return the {@link HttpBasicSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public HttpBasicSpec authenticationSuccessHandler(
|
||||
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
|
||||
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
|
||||
handlersConsumer.accept(this.authenticationSuccessHandlers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to
|
||||
* {@link ServerHttpSecurity#authenticationManager(ReactiveAuthenticationManager)}.
|
||||
@ -2657,17 +2308,9 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
|
||||
authenticationFilter.setAuthenticationConverter(new ServerHttpBasicAuthenticationConverter());
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandlers.isEmpty()) {
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
|
||||
}
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
|
||||
}
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler() {
|
||||
if (this.authenticationFailureHandler != null) {
|
||||
return this.authenticationFailureHandler;
|
||||
@ -2739,9 +2382,6 @@ public class ServerHttpSecurity {
|
||||
private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
|
||||
"/");
|
||||
|
||||
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
|
||||
List.of(this.defaultSuccessHandler));
|
||||
|
||||
private RedirectServerAuthenticationEntryPoint defaultEntryPoint;
|
||||
|
||||
private ReactiveAuthenticationManager authenticationManager;
|
||||
@ -2756,7 +2396,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = this.defaultSuccessHandler;
|
||||
|
||||
private FormLoginSpec() {
|
||||
}
|
||||
@ -2774,34 +2414,14 @@ public class ServerHttpSecurity {
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}. Note
|
||||
* that this method clears previously added success handlers via
|
||||
* {@link #authenticationSuccessHandler(Consumer)}
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}.
|
||||
* @param authenticationSuccessHandler the success handler to use
|
||||
* @return the {@link FormLoginSpec} to continue configuring
|
||||
*/
|
||||
public FormLoginSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
authenticationSuccessHandler((handlers) -> {
|
||||
handlers.clear();
|
||||
handlers.add(authenticationSuccessHandler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
|
||||
* default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
|
||||
* redirects to "/".
|
||||
* @param handlersConsumer the handlers consumer
|
||||
* @return the {@link FormLoginSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public FormLoginSpec authenticationSuccessHandler(
|
||||
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
|
||||
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
|
||||
handlersConsumer.accept(this.authenticationSuccessHandlers);
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -2934,18 +2554,11 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setRequiresAuthenticationMatcher(this.requiresAuthenticationMatcher);
|
||||
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
|
||||
authenticationFilter.setAuthenticationConverter(new ServerFormLoginAuthenticationConverter());
|
||||
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
|
||||
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN);
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandlers.isEmpty()) {
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
|
||||
}
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class LoginPageSpec {
|
||||
@ -4124,12 +3737,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ReactiveOidcSessionRegistry oidcSessionRegistry;
|
||||
|
||||
private final RedirectServerAuthenticationSuccessHandler defaultAuthenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
||||
|
||||
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
|
||||
List.of(this.defaultAuthenticationSuccessHandler));
|
||||
|
||||
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
@ -4177,8 +3785,7 @@ public class ServerHttpSecurity {
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
|
||||
* redirecting to "/". Note that this method clears previously added success
|
||||
* handlers via {@link #authenticationSuccessHandler(Consumer)}
|
||||
* redirecting to "/".
|
||||
* @param authenticationSuccessHandler the success handler to use
|
||||
* @return the {@link OAuth2LoginSpec} to customize
|
||||
* @since 5.2
|
||||
@ -4186,25 +3793,7 @@ public class ServerHttpSecurity {
|
||||
public OAuth2LoginSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
authenticationSuccessHandler((handlers) -> {
|
||||
handlers.clear();
|
||||
handlers.add(authenticationSuccessHandler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
|
||||
* default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
|
||||
* redirects to "/".
|
||||
* @param handlersConsumer the handlers consumer
|
||||
* @return the {@link OAuth2LoginSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public OAuth2LoginSpec authenticationSuccessHandler(
|
||||
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
|
||||
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
|
||||
handlersConsumer.accept(this.authenticationSuccessHandlers);
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -4461,11 +4050,12 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
this.defaultAuthenticationSuccessHandler.setRequestCache(http.requestCache.requestCache);
|
||||
if (this.authenticationSuccessHandlers.isEmpty()) {
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
|
||||
if (this.authenticationSuccessHandler == null) {
|
||||
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
|
||||
handler.setRequestCache(http.requestCache.requestCache);
|
||||
this.authenticationSuccessHandler = handler;
|
||||
}
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
|
||||
return this.authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@ -26,7 +26,6 @@ import org.springframework.security.config.annotation.web.configurers.AuthorizeH
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
|
||||
import org.springframework.security.web.access.IpAddressAuthorizationManager
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||
@ -223,13 +222,6 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() {
|
||||
return AuthorityAuthorizationManager.hasAnyRole(*roles)
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a specific IP or range of IP addresses.
|
||||
* @since 6.3
|
||||
*/
|
||||
fun hasIpAddress(ipAddress: String): AuthorizationManager<RequestAuthorizationContext> =
|
||||
IpAddressAuthorizationManager.hasIpAddress(ipAddress)
|
||||
|
||||
/**
|
||||
* Specify that URLs are allowed by anyone.
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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,8 +38,6 @@ import jakarta.servlet.http.HttpServletRequest
|
||||
* @property loginProcessingUrl the URL to validate the credentials
|
||||
* @property permitAll whether to grant access to the urls for [failureUrl] as well as
|
||||
* for the [HttpSecurityBuilder], the [loginPage] and [loginProcessingUrl] for every user
|
||||
* @property usernameParameter the HTTP parameter to look for the username when performing authentication
|
||||
* @property passwordParameter the HTTP parameter to look for the password when performing authentication
|
||||
*/
|
||||
@SecurityMarker
|
||||
class FormLoginDsl {
|
||||
@ -50,8 +48,6 @@ class FormLoginDsl {
|
||||
var loginProcessingUrl: String? = null
|
||||
var permitAll: Boolean? = null
|
||||
var authenticationDetailsSource: AuthenticationDetailsSource<HttpServletRequest, *>? = null
|
||||
var usernameParameter: String? = null
|
||||
var passwordParameter: String? = null
|
||||
|
||||
private var defaultSuccessUrlOption: Pair<String, Boolean>? = null
|
||||
|
||||
@ -99,8 +95,6 @@ class FormLoginDsl {
|
||||
authenticationSuccessHandler?.also { login.successHandler(authenticationSuccessHandler) }
|
||||
authenticationFailureHandler?.also { login.failureHandler(authenticationFailureHandler) }
|
||||
authenticationDetailsSource?.also { login.authenticationDetailsSource(authenticationDetailsSource) }
|
||||
usernameParameter?.also { login.usernameParameter(usernameParameter) }
|
||||
passwordParameter?.also { login.passwordParameter(passwordParameter) }
|
||||
if (disabled) {
|
||||
login.disable()
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -707,69 +707,6 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
|
||||
this.http.saml2Login(saml2LoginCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures logout support for a SAML 2.0 Service Provider. <br>
|
||||
* <br>
|
||||
*
|
||||
* Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
|
||||
* documented in the
|
||||
* <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
|
||||
* Core, Profiles and Bindings</a> specifications. <br>
|
||||
* <br>
|
||||
*
|
||||
* As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting
|
||||
* Party to send a logout request to. The representation of the relying party and the
|
||||
* asserting party is contained within [RelyingPartyRegistration]. <br>
|
||||
* <br>
|
||||
*
|
||||
* [RelyingPartyRegistration] (s) are composed within a
|
||||
* [RelyingPartyRegistrationRepository], which is <b>required</b> and must be
|
||||
* registered with the [ApplicationContext] or configured via
|
||||
* [HttpSecurityDsl.saml2Login].<br>
|
||||
* <br>
|
||||
*
|
||||
* The default configuration provides an auto-generated logout endpoint at
|
||||
* `/logout` and redirects to `/login?logout` when
|
||||
* logout completes. <br>
|
||||
* <br>
|
||||
*
|
||||
* <p>
|
||||
* <h2>Example Configuration</h2>
|
||||
*
|
||||
* The following example shows the minimal configuration required, using a
|
||||
* hypothetical asserting party.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
* http {
|
||||
* saml2Login {
|
||||
* relyingPartyRegistration = getSaml2RelyingPartyRegistration()
|
||||
* }
|
||||
* saml2Logout { }
|
||||
* }
|
||||
* return http.build()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* <p>
|
||||
* @param saml2LogoutConfiguration custom configuration to configure the
|
||||
* SAML 2.0 service provider
|
||||
* @since 6.3
|
||||
* @see [Saml2LogoutDsl]
|
||||
*/
|
||||
fun saml2Logout(saml2LogoutConfiguration: Saml2LogoutDsl.() -> Unit) {
|
||||
val saml2LogoutCustomizer = Saml2LogoutDsl().apply(saml2LogoutConfiguration).get()
|
||||
this.http.saml2Logout(saml2LogoutCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a SAML 2.0 relying party metadata endpoint.
|
||||
*
|
||||
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
|
||||
import org.springframework.security.config.annotation.web.saml2.LogoutRequestDsl
|
||||
import org.springframework.security.config.annotation.web.saml2.LogoutResponseDsl
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [HttpSecurity] SAML2 logout using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.3
|
||||
* @property relyingPartyRegistrationRepository the [RelyingPartyRegistrationRepository] of relying parties,
|
||||
* each party representing a service provider, SP and this host, and identity provider, IDP pair that
|
||||
* communicate with each other.
|
||||
* @property logoutUrl the logout page to begin the SLO redirect flow
|
||||
*/
|
||||
@SecurityMarker
|
||||
class Saml2LogoutDsl {
|
||||
var relyingPartyRegistrationRepository: RelyingPartyRegistrationRepository? = null
|
||||
var logoutUrl: String? = null
|
||||
|
||||
private var logoutRequest: ((Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer) -> Unit)? = null
|
||||
private var logoutResponse: ((Saml2LogoutConfigurer<HttpSecurity>.LogoutResponseConfigurer) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Configures SAML 2.0 Logout Request components
|
||||
* @param logoutRequestConfig the {@link Customizer} to provide more
|
||||
* options for the {@link LogoutRequestConfigurer}
|
||||
*/
|
||||
fun logoutRequest(logoutRequestConfig: LogoutRequestDsl.() -> Unit) {
|
||||
this.logoutRequest = LogoutRequestDsl().apply(logoutRequestConfig).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures SAML 2.0 Logout Response components
|
||||
* @param logoutResponseConfig the {@link Customizer} to provide more
|
||||
* options for the {@link LogoutResponseConfigurer}
|
||||
*/
|
||||
fun logoutResponse(logoutResponseConfig: LogoutResponseDsl.() -> Unit) {
|
||||
this.logoutResponse = LogoutResponseDsl().apply(logoutResponseConfig).get()
|
||||
}
|
||||
|
||||
internal fun get(): (Saml2LogoutConfigurer<HttpSecurity>) -> Unit {
|
||||
return { saml2Logout ->
|
||||
relyingPartyRegistrationRepository?.also { saml2Logout.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository) }
|
||||
logoutUrl?.also { saml2Logout.logoutUrl(logoutUrl) }
|
||||
logoutRequest?.also { saml2Logout.logoutRequest(logoutRequest) }
|
||||
logoutResponse?.also { saml2Logout.logoutResponse(logoutResponse) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.saml2
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure SAML 2.0 Logout Request components using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.3
|
||||
* @property logoutUrl The URL by which the asserting party can send a SAML 2.0 Logout Request.
|
||||
* The Asserting Party should use whatever HTTP method specified in {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
|
||||
* @property logoutRequestValidator the [Saml2LogoutRequestValidator] to use for validating incoming {@code LogoutRequest}s.
|
||||
* @property logoutRequestResolver the [Saml2LogoutRequestResolver] to use for generating outgoing {@code LogoutRequest}s.
|
||||
* @property logoutRequestRepository the [Saml2LogoutRequestRepository] to use for storing outgoing {@code LogoutRequest}s for
|
||||
* linking to the corresponding {@code LogoutResponse} from the asserting party
|
||||
*/
|
||||
@Saml2SecurityMarker
|
||||
class LogoutRequestDsl {
|
||||
var logoutUrl = "/logout/saml2/slo"
|
||||
var logoutRequestValidator: Saml2LogoutRequestValidator? = null
|
||||
var logoutRequestResolver: Saml2LogoutRequestResolver? = null
|
||||
var logoutRequestRepository: Saml2LogoutRequestRepository = HttpSessionLogoutRequestRepository()
|
||||
|
||||
internal fun get(): (Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer) -> Unit {
|
||||
return { logoutRequest ->
|
||||
logoutUrl.also { logoutRequest.logoutUrl(logoutUrl) }
|
||||
logoutRequestValidator?.also { logoutRequest.logoutRequestValidator(logoutRequestValidator) }
|
||||
logoutRequestResolver?.also { logoutRequest.logoutRequestResolver(logoutRequestResolver) }
|
||||
logoutRequestRepository.also { logoutRequest.logoutRequestRepository(logoutRequestRepository) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.saml2
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure SAML 2.0 Logout Response components using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.3
|
||||
* @property logoutUrl The URL by which the asserting party can send a SAML 2.0 Logout Response.
|
||||
* The Asserting Party should use whatever HTTP method specified in {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
|
||||
* @property logoutResponseValidator the [Saml2LogoutResponseValidator] to use for validating incoming {@code LogoutResponse}s.
|
||||
* @property logoutResponseResolver the [Saml2LogoutResponseResolver] to use for generating outgoing {@code LogoutResponse}s.
|
||||
*/
|
||||
@Saml2SecurityMarker
|
||||
class LogoutResponseDsl {
|
||||
var logoutUrl = "/logout/saml2/slo"
|
||||
var logoutResponseValidator: Saml2LogoutResponseValidator? = null
|
||||
var logoutResponseResolver: Saml2LogoutResponseResolver? = null
|
||||
|
||||
internal fun get(): (Saml2LogoutConfigurer<HttpSecurity>.LogoutResponseConfigurer) -> Unit {
|
||||
return { logoutResponse ->
|
||||
logoutUrl.also { logoutResponse.logoutUrl(logoutUrl) }
|
||||
logoutResponseValidator?.also { logoutResponse.logoutResponseValidator(logoutResponseValidator) }
|
||||
logoutResponseResolver?.also { logoutResponse.logoutResponseResolver(logoutResponseResolver) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.saml2
|
||||
|
||||
/**
|
||||
* Marker annotation indicating that the annotated class is part of the SAML 2.0 logout security DSL.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.3
|
||||
*/
|
||||
@DslMarker
|
||||
annotation class Saml2SecurityMarker
|
@ -682,36 +682,6 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
|
||||
this.http.oidcLogout(oidcLogoutCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures Session Management support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebFluxSecurity
|
||||
* open class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
* return http {
|
||||
* sessionManagement {
|
||||
* sessionConcurrency { }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param sessionManagementConfig custom configuration to configure the Session Management
|
||||
* @since 6.3
|
||||
* @see [ServerSessionManagementDsl]
|
||||
*/
|
||||
fun sessionManagement(sessionManagementConfig: ServerSessionManagementDsl.() -> Unit) {
|
||||
val sessionManagementCustomizer = ServerSessionManagementDsl().apply(sessionManagementConfig).get()
|
||||
this.http.sessionManagement(sessionManagementCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all configurations to the provided [ServerHttpSecurity]
|
||||
*/
|
||||
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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.config.web.server
|
||||
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry
|
||||
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler
|
||||
import org.springframework.security.web.server.authentication.SessionLimit
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [ServerHttpSecurity] Session Concurrency support using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerSessionConcurrencyDsl {
|
||||
var maximumSessions: SessionLimit? = null
|
||||
var maximumSessionsExceededHandler: ServerMaximumSessionsExceededHandler? = null
|
||||
var sessionRegistry: ReactiveSessionRegistry? = null
|
||||
|
||||
internal fun get(): (ServerHttpSecurity.SessionManagementSpec.ConcurrentSessionsSpec) -> Unit {
|
||||
return { sessionConcurrency ->
|
||||
maximumSessions?.also {
|
||||
sessionConcurrency.maximumSessions(maximumSessions!!)
|
||||
}
|
||||
maximumSessionsExceededHandler?.also {
|
||||
sessionConcurrency.maximumSessionsExceededHandler(maximumSessionsExceededHandler!!)
|
||||
}
|
||||
sessionRegistry?.also {
|
||||
sessionConcurrency.sessionRegistry(sessionRegistry!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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.config.web.server
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [ServerHttpSecurity] Session Management using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerSessionManagementDsl {
|
||||
private var sessionConcurrency: ((ServerHttpSecurity.SessionManagementSpec.ConcurrentSessionsSpec) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Enables Session Management support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebFluxSecurity
|
||||
* open class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
* return http {
|
||||
* sessionManagement {
|
||||
* sessionConcurrency {
|
||||
* maximumSessions = { authentication -> Mono.just(1) }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support
|
||||
* @see [ServerOidcBackChannelLogoutDsl]
|
||||
*/
|
||||
fun sessionConcurrency(sessionConcurrencyConfig: ServerSessionConcurrencyDsl.() -> Unit) {
|
||||
this.sessionConcurrency = ServerSessionConcurrencyDsl().apply(sessionConcurrencyConfig).get()
|
||||
}
|
||||
|
||||
internal fun get(): (ServerHttpSecurity.SessionManagementSpec) -> Unit {
|
||||
return { sessionManagement ->
|
||||
sessionConcurrency?.also { sessionManagement.concurrentSessions(sessionConcurrency) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd
|
||||
@ -23,8 +22,7 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri
|
||||
http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -248,6 +248,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("getFilesToDeserialize")
|
||||
@Disabled("The feature is only supported for versions >= 6.3")
|
||||
void shouldBeAbleToDeserializeClassFromPreviousVersion(Path filePath) {
|
||||
try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile());
|
||||
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
|
||||
|
@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.authorization.AuthorizationProxyFactory;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link PrePostMethodSecurityConfiguration}.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@SecurityTestExecutionListeners
|
||||
public class AuthorizationProxyConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void proxyWhenNotPreAuthorizedThenDenies() {
|
||||
this.spring.register(DefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::makeToast)
|
||||
.withMessage("Access Denied");
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::extractBread)
|
||||
.withMessage("Access Denied");
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void proxyWhenPreAuthorizedThenAllows() {
|
||||
this.spring.register(DefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
toaster.makeToast();
|
||||
assertThat(toaster.extractBread()).isEqualTo("yummy");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxyReactiveWhenNotPreAuthorizedThenDenies() {
|
||||
this.spring.register(ReactiveDefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
Authentication user = TestAuthentication.authenticatedUser();
|
||||
StepVerifier
|
||||
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
StepVerifier
|
||||
.create(toaster.reactiveExtractBread().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxyReactiveWhenPreAuthorizedThenAllows() {
|
||||
this.spring.register(ReactiveDefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
Authentication admin = TestAuthentication.authenticatedAdmin();
|
||||
StepVerifier
|
||||
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(admin)))
|
||||
.expectNext()
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
@Configuration
|
||||
static class DefaultsConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableReactiveMethodSecurity
|
||||
@Configuration
|
||||
static class ReactiveDefaultsConfig {
|
||||
|
||||
}
|
||||
|
||||
static class Toaster {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
void makeToast() {
|
||||
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
String extractBread() {
|
||||
return "yummy";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
Mono<Void> reactiveMakeToast() {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
Mono<String> reactiveExtractBread() {
|
||||
return Mono.just("yummy");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -18,8 +18,6 @@ package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -47,20 +45,4 @@ public class Authz {
|
||||
return message != null && message.contains(authentication.getName());
|
||||
}
|
||||
|
||||
public AuthorizationResult checkResult(boolean result) {
|
||||
return new AuthzResult(result);
|
||||
}
|
||||
|
||||
public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
|
||||
return Mono.just(checkResult(result));
|
||||
}
|
||||
|
||||
public static class AuthzResult extends AuthorizationDecision {
|
||||
|
||||
public AuthzResult(boolean granted) {
|
||||
super(granted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -16,42 +16,23 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.annotation.security.DenyAll;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.parameters.P;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@MethodSecurityService.Mask("classmask")
|
||||
public interface MethodSecurityService {
|
||||
|
||||
@PreAuthorize("denyAll")
|
||||
@ -72,9 +53,6 @@ public interface MethodSecurityService {
|
||||
@RolesAllowed("ADMIN")
|
||||
String jsr250RolesAllowed();
|
||||
|
||||
@RolesAllowed("USER")
|
||||
String jsr250RolesAllowedUser();
|
||||
|
||||
@Secured({ "ROLE_USER", "RUN_AS_SUPER" })
|
||||
Authentication runAs();
|
||||
|
||||
@ -90,9 +68,6 @@ public interface MethodSecurityService {
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
void preAuthorizeAdmin();
|
||||
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
void preAuthorizeUser();
|
||||
|
||||
@PreAuthorize("hasPermission(#object,'read')")
|
||||
String hasPermission(String object);
|
||||
|
||||
@ -115,248 +90,8 @@ public interface MethodSecurityService {
|
||||
@PostAuthorize("returnObject.size == 2")
|
||||
List<String> manyAnnotations(List<String> array);
|
||||
|
||||
@PreFilter("filterObject != 'DropOnPreFilter'")
|
||||
@PreAuthorize("#list.remove('DropOnPreAuthorize')")
|
||||
@Secured("ROLE_SECURED")
|
||||
@RolesAllowed("JSR250")
|
||||
@PostAuthorize("#list.remove('DropOnPostAuthorize')")
|
||||
@PostFilter("filterObject != 'DropOnPostFilter'")
|
||||
List<String> allAnnotations(List<String> list);
|
||||
|
||||
@RequireUserRole
|
||||
@RequireAdminRole
|
||||
void repeatedAnnotations();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StartMaskingHandlerChild.class)
|
||||
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
String preAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = CardNumberMaskingPostProcessor.class)
|
||||
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = PostMaskingPostProcessor.class)
|
||||
String postAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
String preAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
String preAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@NullDenied(role = "ADMIN")
|
||||
String postAuthorizeDeniedWithNullDenied();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
String postAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
String postAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
String preAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask(returnObject)")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
String postAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@AuthorizeReturnObject
|
||||
UserRecordWithEmailProtected getUserRecordWithEmailProtected();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = UserFallbackDeniedHandler.class)
|
||||
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
|
||||
|
||||
@PreAuthorize("@authz.checkResult(#result)")
|
||||
@PostAuthorize("@authz.checkResult(!#result)")
|
||||
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
|
||||
String checkCustomResult(boolean result);
|
||||
|
||||
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StartMaskingHandlerChild extends StarMaskingHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return super.handleDeniedInvocation(methodInvocation, result) + "-child";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationHandler(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return handle(methodInvocation, authorizationResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationPostProcessor(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation mi, AuthorizationResult authorizationResult) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskValueResolver {
|
||||
|
||||
DefaultMethodSecurityExpressionHandler expressionHandler;
|
||||
|
||||
MaskValueResolver(ApplicationContext context) {
|
||||
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
||||
this.expressionHandler.setApplicationContext(context);
|
||||
}
|
||||
|
||||
String resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
|
||||
if (StringUtils.hasText(mask.value())) {
|
||||
return mask.value();
|
||||
}
|
||||
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
|
||||
EvaluationContext evaluationContext = this.expressionHandler
|
||||
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
|
||||
if (returnObject != null) {
|
||||
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
|
||||
}
|
||||
return expression.getValue(evaluationContext, String.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PostMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
static String MASK = "****-****-****-";
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult contextObject, AuthorizationResult result) {
|
||||
String cardNumber = (String) contextObject.getResult();
|
||||
return MASK + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NullPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface Mask {
|
||||
|
||||
String value() default "";
|
||||
|
||||
String expression() default "";
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@PostAuthorize("hasRole('{role}')")
|
||||
@HandleAuthorizationDenied(handlerClass = NullPostProcessor.class)
|
||||
@interface NullDenied {
|
||||
|
||||
String role();
|
||||
|
||||
}
|
||||
|
||||
class UserFallbackDeniedHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
private static final UserRecordWithEmailProtected FALLBACK = new UserRecordWithEmailProtected("Protected",
|
||||
"Protected");
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,14 +28,4 @@ public class MethodSecurityServiceConfig {
|
||||
return new MethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMethodSecurityService reactiveService() {
|
||||
return new ReactiveMethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@ -18,8 +18,6 @@ package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
@ -58,11 +56,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String jsr250RolesAllowedUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication runAs() {
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
@ -80,10 +73,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
public void preAuthorizeAdmin() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preAuthorizeUser() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizePermitAll() {
|
||||
return null;
|
||||
@ -119,88 +108,8 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> allAnnotations(List<String> list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repeatedAnnotations() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
|
||||
return cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeThrowAccessDeniedManually() {
|
||||
throw new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeThrowAccessDeniedManually() {
|
||||
throw new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeDeniedWithNullDenied() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String postAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserRecordWithEmailProtected getUserRecordWithEmailProtected() {
|
||||
return new UserRecordWithEmailProtected("username", "useremail@example.com");
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
|
||||
return new UserRecordWithEmailProtected("username", "useremail@example.com");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkCustomResult(boolean result) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
public class MyMasker {
|
||||
|
||||
public String getMask(String value) {
|
||||
return value + "-masked";
|
||||
}
|
||||
|
||||
public String getMask() {
|
||||
return "mask";
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -17,14 +17,9 @@
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@ -41,7 +36,6 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
@ -52,24 +46,12 @@ import org.springframework.security.access.annotation.ExpressionProtectedBusines
|
||||
import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
|
||||
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
@ -88,12 +70,9 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
@ -460,6 +439,7 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse();
|
||||
}
|
||||
|
||||
// gh-13572
|
||||
@Test
|
||||
public void configureWhenBeanOverridingDisallowedThenWorks() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, BusinessServiceConfig.class)
|
||||
@ -467,506 +447,10 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
.autowire();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void methodSecurityAdminWhenRoleHierarchyBeanAvailableThenUses() {
|
||||
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void methodSecurityUserWhenRoleHierarchyBeanAvailableThenUses() {
|
||||
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void methodSecurityAdminWhenAuthorizationEventPublisherBeanAvailableThenUses() {
|
||||
this.spring
|
||||
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
|
||||
AuthorizationEventPublisherConfig.class)
|
||||
.autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void methodSecurityUserWhenAuthorizationEventPublisherBeanAvailableThenUses() {
|
||||
this.spring
|
||||
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
|
||||
AuthorizationEventPublisherConfig.class)
|
||||
.autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPreFilterThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPreFilterConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(5);
|
||||
assertThat(filtered).containsExactly("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPreAuthorizeThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPreAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(4);
|
||||
assertThat(filtered).containsExactly("DropOnPreAuthorize", "DropOnPostAuthorize", "DropOnPostFilter",
|
||||
"DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetSecuredThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetSecuredConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetJsr250WithInsufficientRolesThenFails() {
|
||||
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "SECURED")
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetJsr250ThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeWithInsufficientRolesThenFails() {
|
||||
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostFilterThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPostFilterConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(2);
|
||||
assertThat(filtered).containsExactly("DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnAfterAllOffsetConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(1);
|
||||
assertThat(filtered).containsExactly("DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.hasRole("USER")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.hasUserRole()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void methodWhenParameterizedAnnotationThenFails() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "SCOPE_message:read")
|
||||
public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.readMessage()).isEqualTo("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
public void methodWhenMultiplePlaceholdersHasRoleThenPasses() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.readMessage()).isEqualTo("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
service.startsWithDave("daveMatthews");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> service.startsWithDave("jenniferHarper"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodWhenPreFilterMetaAnnotationThenFilters() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
|
||||
.containsExactly("dave");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void methodWhenPostFilterMetaAnnotationThenFilters() {
|
||||
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
|
||||
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
|
||||
assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
|
||||
.containsExactly("dave");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findByIdWhenAuthorizedResultThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.findById("1");
|
||||
assertThatNoException().isThrownBy(flight::getAltitude);
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findByIdWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.findById("1");
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findAllWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll().forEachRemaining((flight) -> {
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeWhenAuthorizedResultThenRemoves() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.remove("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findAllWhenPostFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll()
|
||||
.forEachRemaining((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
|
||||
.doesNotContain("Kevin Mitnick"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findAllWhenPreFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll().forEachRemaining((flight) -> {
|
||||
flight.board(new ArrayList<>(List.of("John")));
|
||||
assertThat(flight.getPassengers()).extracting(Passenger::getName).doesNotContain("John");
|
||||
flight.board(new ArrayList<>(List.of("John Doe")));
|
||||
assertThat(flight.getPassengers()).extracting(Passenger::getName).contains("John Doe");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
flights.findAll().forEachRemaining((flight) -> {
|
||||
List<Passenger> passengers = flight.getPassengers();
|
||||
passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(passenger::getName));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
MethodSecurityService.CardNumberMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
|
||||
assertThat(cardNumber).isEqualTo("****-****-****-1111");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
|
||||
assertThat(cardNumber).isEqualTo("***");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class,
|
||||
MethodSecurityService.StartMaskingHandlerChild.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
|
||||
assertThat(cardNumber).isEqualTo("***-child");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenHandled() {
|
||||
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
assertThat(service.preAuthorizeThrowAccessDeniedManually()).isEqualTo("***");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenHandled() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.PostMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
assertThat(service.postAuthorizeThrowAccessDeniedManually()).isEqualTo("***");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
assertThat(result).isEqualTo("methodmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
assertThat(result).isEqualTo("classmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MetaAnnotationPlaceholderConfig.class,
|
||||
MethodSecurityService.NullPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeDeniedWithNullDenied();
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
assertThat(result).isEqualTo("methodmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
assertThat(result).isEqualTo("classmask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("ok-masked");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("mask");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
|
||||
MyMasker.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
|
||||
assertThat(result).isEqualTo("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenAuthorizedAndUserEmailIsProtectedAndNotAuthorizedThenReturnEmailMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
UserRecordWithEmailProtected.EmailMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
UserRecordWithEmailProtected user = service.getUserRecordWithEmailProtected();
|
||||
assertThat(user.email()).isEqualTo("use******@example.com");
|
||||
assertThat(user.name()).isEqualTo("username");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.UserFallbackDeniedHandler.class)
|
||||
.autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
UserRecordWithEmailProtected user = service.getUserWithFallbackWhenUnauthorized();
|
||||
assertThat(user.email()).isEqualTo("Protected");
|
||||
assertThat(user.name()).isEqualTo("Protected");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
|
||||
.getBean(MethodAuthorizationDeniedHandler.class);
|
||||
assertThat(service.checkCustomResult(false)).isNull();
|
||||
verify(handler).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
clearInvocations(handler);
|
||||
assertThat(service.checkCustomResult(true)).isNull();
|
||||
verify(handler).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
private static Advisor returnAdvisor(int order) {
|
||||
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
|
||||
pointcut.setPattern(".*MethodSecurityServiceImpl.*");
|
||||
MethodInterceptor interceptor = (mi) -> mi.getArguments()[0];
|
||||
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor);
|
||||
advisor.setOrder(order);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AuthzConfig {
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCustomMethodSecurity
|
||||
static class CustomMethodSecurityServiceConfig {
|
||||
@ -1143,341 +627,4 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
|
||||
static class RoleHierarchyConfig {
|
||||
|
||||
@Bean
|
||||
static RoleHierarchy roleHierarchy() {
|
||||
RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
|
||||
roleHierarchyImpl.setHierarchy("ROLE_ADMIN > ROLE_USER");
|
||||
return roleHierarchyImpl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPreFilterConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreFilter() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPreAuthorizeConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreAuthorize() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetSecuredConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforeSecured() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.SECURED.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetJsr250Config {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforeJsr250() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.JSR250.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPostAuthorizeConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreAuthorize() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPostFilterConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePostFilter() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnAfterAllOffsetConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnAfterAll() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(offset = OffsetConfig.OFFSET, jsr250Enabled = true, securedEnabled = true)
|
||||
static class OffsetConfig {
|
||||
|
||||
static final int OFFSET = 2;
|
||||
|
||||
@Bean
|
||||
MethodSecurityService methodSecurityService() {
|
||||
return new MethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
static class MetaAnnotationPlaceholderConfig {
|
||||
|
||||
@Bean
|
||||
PrePostTemplateDefaults methodSecurityDefaults() {
|
||||
return new PrePostTemplateDefaults();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MetaAnnotationService metaAnnotationService() {
|
||||
return new MetaAnnotationService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MetaAnnotationService {
|
||||
|
||||
@RequireRole(role = "#role")
|
||||
boolean hasRole(String role) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@RequireRole(role = "'USER'")
|
||||
boolean hasUserRole() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole({role})")
|
||||
void placeholdersOnlyResolvedByMetaAnnotations() {
|
||||
}
|
||||
|
||||
@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
|
||||
String readMessage() {
|
||||
return "message";
|
||||
}
|
||||
|
||||
@ResultStartsWith("dave")
|
||||
String startsWithDave(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@ParameterContains("dave")
|
||||
List<String> parametersContainDave(List<String> list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
@ResultContains("dave")
|
||||
List<String> resultsContainDave(List<String> list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole({role})")
|
||||
@interface RequireRole {
|
||||
|
||||
String role();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
|
||||
@interface HasClaim {
|
||||
|
||||
String claim();
|
||||
|
||||
String[] roles() default {};
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PostAuthorize("returnObject.startsWith('{value}')")
|
||||
@interface ResultStartsWith {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreFilter("filterObject.contains('{value}')")
|
||||
@interface ParameterContains {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PostFilter("filterObject.contains('{value}')")
|
||||
@interface ResultContains {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
@Configuration
|
||||
static class AuthorizeResultConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
|
||||
return (f) -> f.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
|
||||
}
|
||||
|
||||
@Bean
|
||||
FlightRepository flights() {
|
||||
FlightRepository flights = new FlightRepository();
|
||||
Flight one = new Flight("1", 35000d, 35);
|
||||
one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
|
||||
flights.save(one);
|
||||
Flight two = new Flight("2", 32000d, 72);
|
||||
two.board(new ArrayList<>(List.of("Albert Einstein")));
|
||||
flights.save(two);
|
||||
return flights;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RoleHierarchy roleHierarchy() {
|
||||
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class FlightRepository {
|
||||
|
||||
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
|
||||
|
||||
Iterator<Flight> findAll() {
|
||||
return this.flights.values().iterator();
|
||||
}
|
||||
|
||||
Flight findById(String id) {
|
||||
return this.flights.get(id);
|
||||
}
|
||||
|
||||
Flight save(Flight flight) {
|
||||
this.flights.put(flight.getId(), flight);
|
||||
return flight;
|
||||
}
|
||||
|
||||
void remove(String id) {
|
||||
this.flights.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class Flight {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Double altitude;
|
||||
|
||||
private final Integer seats;
|
||||
|
||||
private final List<Passenger> passengers = new ArrayList<>();
|
||||
|
||||
Flight(String id, Double altitude, Integer seats) {
|
||||
this.id = id;
|
||||
this.altitude = altitude;
|
||||
this.seats = seats;
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
Double getAltitude() {
|
||||
return this.altitude;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('seating:read')")
|
||||
Integer getSeats() {
|
||||
return this.seats;
|
||||
}
|
||||
|
||||
@PostAuthorize("hasAuthority('seating:read')")
|
||||
@PostFilter("filterObject.name != 'Kevin Mitnick'")
|
||||
List<Passenger> getPassengers() {
|
||||
return this.passengers;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('seating:read')")
|
||||
@PreFilter("filterObject.contains(' ')")
|
||||
void board(List<String> passengers) {
|
||||
for (String passenger : passengers) {
|
||||
this.passengers.add(new Passenger(passenger));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Passenger {
|
||||
|
||||
String name;
|
||||
|
||||
public Passenger(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
static class CustomResultConfig {
|
||||
|
||||
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
|
||||
|
||||
@Bean
|
||||
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,215 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@SecurityTestExecutionListeners
|
||||
public class PrePostReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.CardNumberMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
|
||||
.expectNext("****-****-****-1111")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
|
||||
.expectNext("***")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class,
|
||||
ReactiveMethodSecurityService.StartMaskingHandlerChild.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111"))
|
||||
.expectNext("***-child")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeDeniedMethodWithMaskAnnotation())
|
||||
.expectNext("methodmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeDeniedMethodWithNoMaskAnnotation())
|
||||
.expectNext("classmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.PostMaskingPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeThrowAccessDeniedManually()).expectNext("***").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenHandled() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeThrowAccessDeniedManually()).expectNext("***").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.NullPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeDeniedWithNullDenied()).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeDeniedMethodWithMaskAnnotation())
|
||||
.expectNext("methodmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeDeniedMethodWithNoMaskAnnotation())
|
||||
.expectNext("classmask")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean())
|
||||
.expectNext("ok-masked")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("mask").verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
|
||||
this.spring
|
||||
.register(MethodSecurityServiceEnabledConfig.class,
|
||||
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
|
||||
.autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
static class MethodSecurityServiceEnabledConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveMethodSecurityService methodSecurityService() {
|
||||
return new ReactiveMethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,53 +16,22 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.expression.SecurityExpressionRoot;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.intercept.method.MockMethodInvocation;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Tadaya Tsuyukubo
|
||||
@ -72,13 +41,14 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired(required = false)
|
||||
@Autowired
|
||||
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
|
||||
|
||||
@Test
|
||||
public void rolePrefixWithGrantedAuthorityDefaults() throws NoSuchMethodException {
|
||||
this.spring.register(WithRolePrefixConfiguration.class).autowire();
|
||||
Authentication authentication = TestAuthentication.authenticatedUser(authorities("CUSTOM_ABC"));
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
|
||||
"CUSTOM_ABC");
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
|
||||
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
|
||||
methodInvocation);
|
||||
@ -92,7 +62,8 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
@Test
|
||||
public void rolePrefixWithDefaultConfig() throws NoSuchMethodException {
|
||||
this.spring.register(ReactiveMethodSecurityConfiguration.class).autowire();
|
||||
Authentication authentication = TestAuthentication.authenticatedUser(authorities("ROLE_ABC"));
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
|
||||
"ROLE_ABC");
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
|
||||
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
|
||||
methodInvocation);
|
||||
@ -104,7 +75,8 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
@Test
|
||||
public void rolePrefixWithGrantedAuthorityDefaultsAndSubclassWithProxyingEnabled() throws NoSuchMethodException {
|
||||
this.spring.register(SubclassConfig.class).autowire();
|
||||
Authentication authentication = TestAuthentication.authenticatedUser(authorities("ROLE_ABC"));
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
|
||||
"ROLE_ABC");
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
|
||||
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
|
||||
methodInvocation);
|
||||
@ -113,132 +85,6 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
assertThat(root.hasRole("ABC")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdWhenAuthorizedResultThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getAltitude)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getSeats)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getSeats)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
StepVerifier
|
||||
.create(flights.findById("1")
|
||||
.flatMap(Flight::getAltitude)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenUnauthorizedResultThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getSeats)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNextCount(2)
|
||||
.verifyComplete();
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getAltitude)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeWhenAuthorizedResultThenRemoves() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier.create(flights.remove("1").contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenPostFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getPassengers)
|
||||
.flatMap(Passenger::getName)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNext("Marie Curie", "Ada Lovelace", "Albert Einstein")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenPreFilterThenFilters() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap((flight) -> flight.board(Flux.just("John Doe", "John")).then(Mono.just(flight)))
|
||||
.flatMap(Flight::getPassengers)
|
||||
.flatMap(Passenger::getName)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.expectNext("Marie Curie", "Ada Lovelace", "John Doe", "Albert Einstein", "John Doe")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
|
||||
StepVerifier
|
||||
.create(flights.findAll()
|
||||
.flatMap(Flight::getPassengers)
|
||||
.flatMap(Passenger::getName)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
|
||||
.verifyError(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
|
||||
.getBean(MethodAuthorizationDeniedHandler.class);
|
||||
assertThat(service.checkCustomResult(false).block()).isNull();
|
||||
verify(handler).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
clearInvocations(handler);
|
||||
assertThat(service.checkCustomResult(true).block()).isNull();
|
||||
verify(handler).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
|
||||
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
|
||||
}
|
||||
|
||||
private static Consumer<User.UserBuilder> authorities(String... authorities) {
|
||||
return (builder) -> builder.authorities(authorities);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration
|
||||
static class WithRolePrefixConfiguration {
|
||||
@ -262,130 +108,4 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableReactiveMethodSecurity
|
||||
@Configuration
|
||||
static class AuthorizeResultConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
|
||||
return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
|
||||
}
|
||||
|
||||
@Bean
|
||||
FlightRepository flights() {
|
||||
FlightRepository flights = new FlightRepository();
|
||||
Flight one = new Flight("1", 35000d, 35);
|
||||
one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block();
|
||||
flights.save(one).block();
|
||||
Flight two = new Flight("2", 32000d, 72);
|
||||
two.board(Flux.just("Albert Einstein")).block();
|
||||
flights.save(two).block();
|
||||
return flights;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Function<Passenger, Mono<Boolean>> isNotKevin() {
|
||||
return (passenger) -> passenger.getName().map((name) -> !name.equals("Kevin Mitnick"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class FlightRepository {
|
||||
|
||||
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
|
||||
|
||||
Flux<Flight> findAll() {
|
||||
return Flux.fromIterable(this.flights.values());
|
||||
}
|
||||
|
||||
Mono<Flight> findById(String id) {
|
||||
return Mono.just(this.flights.get(id));
|
||||
}
|
||||
|
||||
Mono<Flight> save(Flight flight) {
|
||||
this.flights.put(flight.getId(), flight);
|
||||
return Mono.just(flight);
|
||||
}
|
||||
|
||||
Mono<Void> remove(String id) {
|
||||
this.flights.remove(id);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
static class Flight {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Double altitude;
|
||||
|
||||
private final Integer seats;
|
||||
|
||||
private final List<Passenger> passengers = new ArrayList<>();
|
||||
|
||||
Flight(String id, Double altitude, Integer seats) {
|
||||
this.id = id;
|
||||
this.altitude = altitude;
|
||||
this.seats = seats;
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
Mono<Double> getAltitude() {
|
||||
return Mono.just(this.altitude);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
|
||||
Mono<Integer> getSeats() {
|
||||
return Mono.just(this.seats);
|
||||
}
|
||||
|
||||
@PostAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
|
||||
@PostFilter("@isNotKevin.apply(filterObject)")
|
||||
Flux<Passenger> getPassengers() {
|
||||
return Flux.fromIterable(this.passengers);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
|
||||
@PreFilter("filterObject.contains(' ')")
|
||||
Mono<Void> board(Flux<String> passengers) {
|
||||
return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Passenger {
|
||||
|
||||
String name;
|
||||
|
||||
public Passenger(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('airplane:read')")
|
||||
public Mono<String> getName() {
|
||||
return Mono.just(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableReactiveMethodSecurity
|
||||
static class CustomResultConfig {
|
||||
|
||||
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
|
||||
|
||||
@Bean
|
||||
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,255 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ReactiveMethodSecurityService.Mask("classmask")
|
||||
public interface ReactiveMethodSecurityService {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StartMaskingHandlerChild.class)
|
||||
Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
|
||||
Mono<String> preAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = CardNumberMaskingPostProcessor.class)
|
||||
Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber);
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = PostMaskingPostProcessor.class)
|
||||
Mono<String> postAuthorizeThrowAccessDeniedManually();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PreAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@NullDenied(role = "ADMIN")
|
||||
Mono<String> postAuthorizeDeniedWithNullDenied();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@Mask("methodmask")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation();
|
||||
|
||||
@PostAuthorize("denyAll()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation();
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask()")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
|
||||
Mono<String> preAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@Mask(expression = "@myMasker.getMask(returnObject)")
|
||||
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
|
||||
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
|
||||
|
||||
@PreAuthorize("@authz.checkReactiveResult(#result)")
|
||||
@PostAuthorize("@authz.checkReactiveResult(!#result)")
|
||||
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
|
||||
Mono<String> checkCustomResult(boolean result);
|
||||
|
||||
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StartMaskingHandlerChild extends StarMaskingHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
return super.handleDeniedInvocation(methodInvocation, result) + "-child";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationHandler(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
MaskValueResolver maskValueResolver;
|
||||
|
||||
MaskAnnotationPostProcessor(ApplicationContext context) {
|
||||
this.maskValueResolver = new MaskValueResolver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation mi, AuthorizationResult authorizationResult) {
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
|
||||
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
|
||||
if (mask == null) {
|
||||
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
|
||||
}
|
||||
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaskValueResolver {
|
||||
|
||||
DefaultMethodSecurityExpressionHandler expressionHandler;
|
||||
|
||||
MaskValueResolver(ApplicationContext context) {
|
||||
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
||||
this.expressionHandler.setApplicationContext(context);
|
||||
}
|
||||
|
||||
Mono<String> resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
|
||||
if (StringUtils.hasText(mask.value())) {
|
||||
return Mono.just(mask.value());
|
||||
}
|
||||
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
|
||||
EvaluationContext evaluationContext = this.expressionHandler
|
||||
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
|
||||
if (returnObject != null) {
|
||||
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
|
||||
}
|
||||
return Mono.just(expression.getValue(evaluationContext, String.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PostMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
static String MASK = "****-****-****-";
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult contextObject, AuthorizationResult result) {
|
||||
String cardNumber = (String) contextObject.getResult();
|
||||
return MASK + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NullPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface Mask {
|
||||
|
||||
String value() default "";
|
||||
|
||||
String expression() default "";
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@PostAuthorize("hasRole('{value}')")
|
||||
@HandleAuthorizationDenied(handlerClass = NullPostProcessor.class)
|
||||
@interface NullDenied {
|
||||
|
||||
String role();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
|
||||
public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService {
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return Mono.just(cardNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
|
||||
return Mono.just(cardNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeThrowAccessDeniedManually() {
|
||||
return Mono.error(new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
|
||||
return Mono.just(cardNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeThrowAccessDeniedManually() {
|
||||
return Mono.error(new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeDeniedWithNullDenied() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> postAuthorizeWithMaskAnnotationUsingBean() {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> checkCustomResult(boolean result) {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
|
||||
public class UserRecordWithEmailProtected {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String email;
|
||||
|
||||
public UserRecordWithEmailProtected(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
@HandleAuthorizationDenied(handlerClass = EmailMaskingPostProcessor.class)
|
||||
public String email() {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
public static class EmailMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
|
||||
AuthorizationResult authorizationResult) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
|
||||
AuthorizationResult authorizationResult) {
|
||||
String email = (String) methodInvocationResult.getResult();
|
||||
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration.issue14637;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaRepositories("org.springframework.security.config.annotation.method.configuration.issue14637.repo")
|
||||
@EnableTransactionManagement
|
||||
public class ApplicationConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
|
||||
return builder.setType(EmbeddedDatabaseType.HSQL).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||
vendorAdapter.setDatabase(Database.HSQL);
|
||||
vendorAdapter.setGenerateDdl(true);
|
||||
vendorAdapter.setShowSql(true);
|
||||
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
|
||||
factory.setJpaVendorAdapter(vendorAdapter);
|
||||
factory.setPackagesToScan(Entry.class.getPackage().getName());
|
||||
factory.setDataSource(dataSource());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager() {
|
||||
JpaTransactionManager txManager = new JpaTransactionManager();
|
||||
txManager.setEntityManagerFactory(entityManagerFactory().getObject());
|
||||
return txManager;
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration.issue14637;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.repo.EntryRepository;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = { ApplicationConfig.class, SecurityConfig.class })
|
||||
public class Issue14637Tests {
|
||||
|
||||
@Autowired
|
||||
private EntryRepository entries;
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void authenticateWhenInvalidPasswordThenBadCredentialsException() {
|
||||
Entry entry = new Entry();
|
||||
entry.setId(123L);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.entries.save(entry));
|
||||
}
|
||||
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration.issue14637;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration.issue14637.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@Entity
|
||||
public class Entry {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration.issue14637.repo;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public interface EntryRepository extends CrudRepository<Entry, String> {
|
||||
|
||||
@PreAuthorize("#entry.id == null")
|
||||
Entry save(Entry entry);
|
||||
|
||||
}
|
@ -47,9 +47,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordException;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@ -63,8 +60,8 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.provisioning.UserDetailsManager;
|
||||
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
@ -398,41 +395,6 @@ public class HttpSecurityConfigurationTests {
|
||||
this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() throws Exception {
|
||||
this.spring
|
||||
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
this.mockMvc.perform(formLogin().password("password"))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/login?error"), unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() throws Exception {
|
||||
this.spring
|
||||
.register(SecurityEnabledRedirectIfPasswordExceptionConfig.class, UserDetailsConfig.class,
|
||||
CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
this.mockMvc.perform(formLogin().password("password"))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/reset-password"), unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenSuccess() throws Exception {
|
||||
this.spring
|
||||
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
UserDetailsManager userDetailsManager = this.spring.getContext().getBean(UserDetailsManager.class);
|
||||
UserDetails notCompromisedPwUser = User.withDefaultPasswordEncoder()
|
||||
.username("user2")
|
||||
.password("password2")
|
||||
.roles("USER")
|
||||
.build();
|
||||
userDetailsManager.createUser(notCompromisedPwUser);
|
||||
this.mockMvc.perform(formLogin().user("user2").password("password2"))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class NameController {
|
||||
|
||||
@ -493,7 +455,7 @@ public class HttpSecurityConfigurationTests {
|
||||
static class UserDetailsConfig {
|
||||
|
||||
@Bean
|
||||
InMemoryUserDetailsManager userDetailsService() {
|
||||
UserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
@ -770,52 +732,4 @@ public class HttpSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CompromisedPasswordCheckerConfig {
|
||||
|
||||
@Bean
|
||||
TestCompromisedPasswordChecker compromisedPasswordChecker() {
|
||||
return new TestCompromisedPasswordChecker();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
static class SecurityEnabledRedirectIfPasswordExceptionConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
return http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin((form) -> form
|
||||
.failureHandler((request, response, exception) -> {
|
||||
if (exception instanceof CompromisedPasswordException) {
|
||||
response.sendRedirect("/reset-password");
|
||||
return;
|
||||
}
|
||||
response.sendRedirect("/login?error");
|
||||
})
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestCompromisedPasswordChecker implements CompromisedPasswordChecker {
|
||||
|
||||
@Override
|
||||
public CompromisedPasswordCheckResult check(String password) {
|
||||
if ("password".equals(password)) {
|
||||
return new CompromisedPasswordCheckResult(true);
|
||||
}
|
||||
return new CompromisedPasswordCheckResult(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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,6 +38,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
|
||||
@ -50,7 +52,6 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
@ -58,14 +59,17 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@ -73,10 +77,13 @@ import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ -320,47 +327,6 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
private void testTokenExchangeGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
|
||||
.willReturn(accessTokenResponse);
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(HttpServletRequest.class.getName(), this.request)
|
||||
.attribute(HttpServletResponse.class.getName(), this.response)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(TokenExchangeGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken getExpiredAccessToken() {
|
||||
Instant expiresAt = Instant.now().minusSeconds(60);
|
||||
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
|
||||
@ -387,32 +353,37 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockAuthorizationCodeClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockRefreshTokenClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockClientCredentialsClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockPasswordClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockJwtBearerClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
|
||||
return mock(DefaultOAuth2UserService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
|
||||
return mock(OidcUserService.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -429,35 +400,28 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
@Bean
|
||||
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordOAuth2AuthorizedClientProvider passwordProvider() {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider() {
|
||||
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@ -465,10 +429,21 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
abstract static class OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oauth2Client(Customizer.withDefaults());
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository() {
|
||||
// @formatter:off
|
||||
return new InMemoryClientRegistrationRepository(
|
||||
return new InMemoryClientRegistrationRepository(Arrays.asList(
|
||||
CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.clientId("google-client-id")
|
||||
.clientSecret("google-client-secret")
|
||||
@ -488,15 +463,7 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
.clientId("okta-client-id")
|
||||
.clientSecret("okta-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build(),
|
||||
ClientRegistration.withRegistrationId("auth0")
|
||||
.clientName("Auth0")
|
||||
.clientId("auth0-client-id")
|
||||
.clientSecret("auth0-client-secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.scope("user.read", "user.write")
|
||||
.build());
|
||||
.build()));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@ -527,11 +494,51 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
|
||||
implements OAuth2AccessTokenResponseClient<T> {
|
||||
private static class MockAuthorizationCodeClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) {
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockRefreshTokenClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockClientCredentialsClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockPasswordClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
|
@ -40,10 +40,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@ -545,17 +543,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(request).andExpect(status().isOk());
|
||||
request = get("/user/deny");
|
||||
this.mvc.perform(request).andExpect(status().isUnauthorized());
|
||||
|
||||
UserDetails user = TestAuthentication.withUsername("taehong").build();
|
||||
Authentication authentication = TestAuthentication.authenticated(user);
|
||||
request = get("/v2/user/{username}", user.getUsername()).with(authentication(authentication));
|
||||
this.mvc.perform(request).andExpect(status().isOk());
|
||||
|
||||
request = get("/v2/user/{username}", "withNoAuthentication");
|
||||
this.mvc.perform(request).andExpect(status().isUnauthorized());
|
||||
|
||||
request = get("/v2/user/{username}", "another").with(authentication(authentication));
|
||||
this.mvc.perform(request).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
private static RequestPostProcessor remoteAddress(String remoteAddress) {
|
||||
@ -609,20 +596,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenNotConfigAndAuthenticatedThenRespondsWithForbidden() throws Exception {
|
||||
this.spring.register(NotConfig.class, BasicController.class).autowire();
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user"));
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Exception {
|
||||
this.spring.register(NotConfig.class, BasicController.class).autowire();
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/");
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class GrantedAuthorityDefaultHasRoleConfig {
|
||||
@ -1080,7 +1053,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.requestMatchers("/user/{username}").access(new WebExpressionAuthorizationManager("#username == 'user'"))
|
||||
.requestMatchers("/v2/user/{username}").hasVariable("username").equalTo(Authentication::getName)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
@ -1094,11 +1066,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
return username;
|
||||
}
|
||||
|
||||
@RequestMapping("/v2/user/{username}")
|
||||
String pathV2(@PathVariable("username") String username) {
|
||||
return username;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1169,24 +1136,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class NotConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain chain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().not().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AuthorizationEventPublisherConfig {
|
||||
|
||||
|
@ -1,589 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
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.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
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.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for
|
||||
* {@link ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class ReactiveOAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
private static ReactiveOAuth2AccessTokenResponseClient<? super AbstractOAuth2AuthorizationGrantRequest> MOCK_RESPONSE_CLIENT;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
@Autowired
|
||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider;
|
||||
|
||||
private MockServerWebExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
MOCK_RESPONSE_CLIENT = mock(ReactiveOAuth2AccessTokenResponseClient.class);
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
||||
this.exchange = MockServerWebExchange.builder(request).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadContextWhenOAuth2ClientEnabledThenConfigured() {
|
||||
this.spring.register(MinimalOAuth2ClientConfig.class).autowire();
|
||||
assertThat(this.authorizedClientManager).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenAuthorizationCodeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId("google")
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
|
||||
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
|
||||
.extracting(OAuth2AuthorizationException::getError)
|
||||
.extracting(OAuth2Error::getErrorCode)
|
||||
.isEqualTo("client_authorization_required");
|
||||
// @formatter:on
|
||||
|
||||
verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenAuthorizedClientServiceBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientServiceConfig.class).autowire();
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId("google")
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
|
||||
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
|
||||
.extracting(OAuth2AuthorizationException::getError)
|
||||
.extracting(OAuth2Error::getErrorCode)
|
||||
.isEqualTo("client_authorization_required");
|
||||
// @formatter:on
|
||||
|
||||
verify(this.authorizedClientService).loadAuthorizedClient(authorizeRequest.getClientRegistrationId(),
|
||||
authentication.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenRefreshTokenAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testRefreshTokenGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenRefreshTokenAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testRefreshTokenGrant();
|
||||
}
|
||||
|
||||
private void testRefreshTokenGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google")
|
||||
.block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
|
||||
authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken());
|
||||
this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.exchange)
|
||||
.block();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2RefreshTokenGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2RefreshTokenGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
OAuth2RefreshTokenGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN);
|
||||
assertThat(grantRequest.getAccessToken()).isEqualTo(existingAuthorizedClient.getAccessToken());
|
||||
assertThat(grantRequest.getRefreshToken()).isEqualTo(existingAuthorizedClient.getRefreshToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientCredentialsAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testClientCredentialsGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenClientCredentialsAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testClientCredentialsGrant();
|
||||
}
|
||||
|
||||
private void testClientCredentialsGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2ClientCredentialsGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github")
|
||||
.block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2ClientCredentialsGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2ClientCredentialsGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
OAuth2ClientCredentialsGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenPasswordAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testPasswordGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenPasswordAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testPasswordGrant();
|
||||
}
|
||||
|
||||
private void testPasswordGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2PasswordGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook")
|
||||
.block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body("username=user&password=password");
|
||||
this.exchange = MockServerWebExchange.builder(request).build();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2PasswordGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2PasswordGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
OAuth2PasswordGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD);
|
||||
assertThat(grantRequest.getUsername()).isEqualTo("user");
|
||||
assertThat(grantRequest.getPassword()).isEqualTo("password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenJwtBearerAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testJwtBearerGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenJwtBearerAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testJwtBearerGrant();
|
||||
}
|
||||
|
||||
private void testJwtBearerGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(JwtBearerGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta").block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<JwtBearerGrantRequest> grantRequestCaptor = ArgumentCaptor.forClass(JwtBearerGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
JwtBearerGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
|
||||
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
private void testTokenExchangeGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
|
||||
.willReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0").block();
|
||||
assertThat(clientRegistration).isNotNull();
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(ServerWebExchange.class.getName(), this.exchange)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(TokenExchangeGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken getExpiredAccessToken() {
|
||||
Instant expiresAt = Instant.now().minusSeconds(60);
|
||||
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
|
||||
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", issuedAt, expiresAt,
|
||||
new HashSet<>(Arrays.asList("read", "write")));
|
||||
}
|
||||
|
||||
private static Jwt getJwt() {
|
||||
Instant issuedAt = Instant.now();
|
||||
return new Jwt("token", issuedAt, issuedAt.plusSeconds(300),
|
||||
Collections.singletonMap(JoseHeaderNames.ALG, "RS256"),
|
||||
Collections.singletonMap(JwtClaimNames.SUB, "user"));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return new WebSessionServerOAuth2AuthorizedClientRepository();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class CustomAuthorizedClientServiceConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService() {
|
||||
ReactiveOAuth2AuthorizedClientService authorizedClientService = mock(
|
||||
ReactiveOAuth2AuthorizedClientService.class);
|
||||
given(authorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(Mono.empty());
|
||||
return authorizedClientService;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class CustomAccessTokenResponseClientsConfig extends MinimalOAuth2ClientConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenAccessResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
static class CustomAuthorizedClientProvidersConfig extends MinimalOAuth2ClientConfig {
|
||||
|
||||
@Bean
|
||||
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCode() {
|
||||
return spy(new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider());
|
||||
}
|
||||
|
||||
@Bean
|
||||
RefreshTokenReactiveOAuth2AuthorizedClientProvider refreshToken() {
|
||||
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientCredentialsReactiveOAuth2AuthorizedClientProvider clientCredentials() {
|
||||
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordReactiveOAuth2AuthorizedClientProvider password() {
|
||||
PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearer() {
|
||||
JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchange() {
|
||||
TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract static class OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||
// @formatter:off
|
||||
return new InMemoryReactiveClientRegistrationRepository(
|
||||
CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.clientId("google-client-id")
|
||||
.clientSecret("google-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.build(),
|
||||
CommonOAuth2Provider.GITHUB.getBuilder("github")
|
||||
.clientId("github-client-id")
|
||||
.clientSecret("github-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.build(),
|
||||
CommonOAuth2Provider.FACEBOOK.getBuilder("facebook")
|
||||
.clientId("facebook-client-id")
|
||||
.clientSecret("facebook-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.build(),
|
||||
CommonOAuth2Provider.OKTA.getBuilder("okta")
|
||||
.clientId("okta-client-id")
|
||||
.clientSecret("okta-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build(),
|
||||
ClientRegistration.withRegistrationId("auth0")
|
||||
.clientName("Auth0")
|
||||
.clientId("auth0-client-id")
|
||||
.clientSecret("auth0-client-secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.scope("user.read", "user.write")
|
||||
.build());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
Consumer<DefaultReactiveOAuth2AuthorizedClientManager> authorizedClientManagerConsumer() {
|
||||
return (authorizedClientManager) -> authorizedClientManager
|
||||
.setContextAttributesMapper((authorizeRequest) -> {
|
||||
ServerWebExchange exchange = Objects
|
||||
.requireNonNull(authorizeRequest.getAttribute(ServerWebExchange.class.getName()));
|
||||
return exchange.getFormData().map((parameters) -> {
|
||||
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||
|
||||
Map<String, Object> attributes = Collections.emptyMap();
|
||||
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
|
||||
attributes = new HashMap<>();
|
||||
attributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
|
||||
attributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
|
||||
implements ReactiveOAuth2AccessTokenResponseClient<T> {
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AccessTokenResponse> getTokenResponse(T grantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(grantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -16,39 +16,16 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordCheckResult;
|
||||
import org.springframework.security.authentication.password.CompromisedPasswordException;
|
||||
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServerHttpSecurityConfiguration}.
|
||||
@ -60,16 +37,6 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
WebTestClient webClient;
|
||||
|
||||
@Autowired
|
||||
void setup(ApplicationContext context) {
|
||||
if (!context.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) {
|
||||
return;
|
||||
}
|
||||
this.webClient = WebTestClient.bindToApplicationContext(context).configureClient().build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigWhenReactiveUserDetailsServiceConfiguredThenServerHttpSecurityExists() {
|
||||
this.spring
|
||||
@ -90,151 +57,9 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
assertThat(serverHttpSecurity).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() {
|
||||
this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// @formatter:off
|
||||
this.webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().location("/login?error");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenUnauthorized() {
|
||||
this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "admin");
|
||||
data.add("password", "password2");
|
||||
// @formatter:off
|
||||
this.webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().location("/");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() {
|
||||
this.spring
|
||||
.register(FormLoginRedirectToResetPasswordConfig.class, UserDetailsConfig.class,
|
||||
CompromisedPasswordCheckerConfig.class)
|
||||
.autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// @formatter:off
|
||||
this.webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().location("/reset-password");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class SubclassConfig extends ServerHttpSecurityConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
static class FormLoginConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchange) -> exchange
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
static class FormLoginRedirectToResetPasswordConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchange) -> exchange
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.formLogin((form) -> form
|
||||
.authenticationFailureHandler((webFilterExchange, exception) -> {
|
||||
String redirectUrl = "/login?error";
|
||||
if (exception instanceof CompromisedPasswordException) {
|
||||
redirectUrl = "/reset-password";
|
||||
}
|
||||
return new DefaultServerRedirectStrategy().sendRedirect(webFilterExchange.getExchange(), URI.create(redirectUrl));
|
||||
})
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class UserDetailsConfig {
|
||||
|
||||
@Bean
|
||||
MapReactiveUserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = PasswordEncodedUser.user();
|
||||
UserDetails admin = User.withDefaultPasswordEncoder()
|
||||
.username("admin")
|
||||
.password("password2")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
// @formatter:on
|
||||
return new MapReactiveUserDetailsService(user, admin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CompromisedPasswordCheckerConfig {
|
||||
|
||||
@Bean
|
||||
TestReactivePasswordChecker compromisedPasswordChecker() {
|
||||
return new TestReactivePasswordChecker();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestReactivePasswordChecker implements ReactiveCompromisedPasswordChecker {
|
||||
|
||||
@Override
|
||||
public Mono<CompromisedPasswordCheckResult> check(String password) {
|
||||
if ("password".equals(password)) {
|
||||
return Mono.just(new CompromisedPasswordCheckResult(true));
|
||||
}
|
||||
return Mono.just(new CompromisedPasswordCheckResult(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ public class XsdDocumentedTests {
|
||||
|
||||
String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd";
|
||||
|
||||
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.3.xsd";
|
||||
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.2.xsd";
|
||||
|
||||
XmlSupport xml = new XmlSupport();
|
||||
|
||||
@ -151,8 +151,8 @@ public class XsdDocumentedTests {
|
||||
.list((dir, name) -> name.endsWith(".xsd"));
|
||||
// @formatter:on
|
||||
assertThat(schemas.length)
|
||||
.withFailMessage("the count is equal to 25, if not then schemaDocument needs updating")
|
||||
.isEqualTo(25);
|
||||
.withFailMessage("the count is equal to 24, if not then schemaDocument needs updating")
|
||||
.isEqualTo(24);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -49,20 +49,18 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@ -318,47 +316,6 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
|
||||
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
|
||||
this.spring.configLocations(xml("clients")).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
|
||||
this.spring.configLocations(xml("providers")).autowire();
|
||||
testTokenExchangeGrant();
|
||||
}
|
||||
|
||||
private void testTokenExchangeGrant() {
|
||||
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
|
||||
.willReturn(accessTokenResponse);
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0");
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(clientRegistration.getRegistrationId())
|
||||
.principal(authentication)
|
||||
.attribute(HttpServletRequest.class.getName(), this.request)
|
||||
.attribute(HttpServletResponse.class.getName(), this.response)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
|
||||
.forClass(TokenExchangeGrantRequest.class);
|
||||
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
|
||||
|
||||
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
|
||||
assertThat(grantRequest.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken getExpiredAccessToken() {
|
||||
Instant expiresAt = Instant.now().minusSeconds(60);
|
||||
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
|
||||
@ -399,14 +356,6 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
|
||||
.clientId("okta-client-id")
|
||||
.clientSecret("okta-client-secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
|
||||
.build(),
|
||||
ClientRegistration.withRegistrationId("auth0")
|
||||
.clientName("Auth0")
|
||||
.clientId("auth0-client-id")
|
||||
.clientSecret("auth0-client-secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.scope("user.read", "user.write")
|
||||
.build());
|
||||
// @formatter:on
|
||||
}
|
||||
@ -429,65 +378,95 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
|
||||
});
|
||||
}
|
||||
|
||||
public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCode() {
|
||||
public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider() {
|
||||
return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider());
|
||||
}
|
||||
|
||||
public static RefreshTokenOAuth2AuthorizedClientProvider refreshToken() {
|
||||
public static RefreshTokenOAuth2AuthorizedClientProvider refreshTokenAuthorizedClientProvider() {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(refreshTokenAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
public static MockRefreshTokenClient refreshTokenAccessTokenResponseClient() {
|
||||
return new MockRefreshTokenClient();
|
||||
}
|
||||
|
||||
public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentials() {
|
||||
public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsAuthorizedClientProvider() {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(clientCredentialsAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockClientCredentialsClient();
|
||||
}
|
||||
|
||||
public static PasswordOAuth2AuthorizedClientProvider password() {
|
||||
public static PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider() {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(passwordAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockPasswordClient();
|
||||
}
|
||||
|
||||
public static JwtBearerOAuth2AuthorizedClientProvider jwtBearer() {
|
||||
public static JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
return new MockJwtBearerClient();
|
||||
}
|
||||
|
||||
public static TokenExchangeOAuth2AuthorizedClientProvider tokenExchange() {
|
||||
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
public static OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
|
||||
return new MockAccessTokenResponseClient<>();
|
||||
}
|
||||
|
||||
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
|
||||
implements OAuth2AccessTokenResponseClient<T> {
|
||||
private static class MockAuthorizationCodeClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) {
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockRefreshTokenClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockClientCredentialsClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(
|
||||
OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockPasswordClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) {
|
||||
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.config.saml2;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@ -24,21 +23,16 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link RelyingPartyRegistrationsBeanDefinitionParser}.
|
||||
@ -124,7 +118,6 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
// @formatter:on
|
||||
|
||||
@Autowired
|
||||
@Qualifier("registrations")
|
||||
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
@ -275,19 +268,6 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWhenRelayStateResolverThenUses() {
|
||||
this.spring.configLocations(xml("RelayStateResolver")).autowire();
|
||||
Converter<HttpServletRequest, String> relayStateResolver = this.spring.getContext().getBean(Converter.class);
|
||||
OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = this.spring.getContext()
|
||||
.getBean(OpenSaml4AuthenticationRequestResolver.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRequestURI("/saml2/authenticate/one");
|
||||
request.setServletPath("/saml2/authenticate/one");
|
||||
authenticationRequestResolver.resolve(request);
|
||||
verify(relayStateResolver).convert(request);
|
||||
}
|
||||
|
||||
private static MockResponse xmlResponse(String xml) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).setBody(xml);
|
||||
}
|
||||
|
@ -56,11 +56,9 @@ import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
|
||||
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
|
||||
@ -594,7 +592,6 @@ public class ServerHttpSecurityTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
|
||||
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
@ -616,11 +613,8 @@ public class ServerHttpSecurityTests {
|
||||
OAuth2LoginAuthenticationWebFilter authenticationWebFilter = getWebFilter(securityFilterChain,
|
||||
OAuth2LoginAuthenticationWebFilter.class)
|
||||
.get();
|
||||
DelegatingServerAuthenticationSuccessHandler handler = (DelegatingServerAuthenticationSuccessHandler) ReflectionTestUtils
|
||||
.getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
List<ServerAuthenticationSuccessHandler> delegates = (List<ServerAuthenticationSuccessHandler>) ReflectionTestUtils
|
||||
.getField(handler, "delegates");
|
||||
assertThat(ReflectionTestUtils.getField(delegates.get(0), "requestCache")).isSameAs(requestCache);
|
||||
Object handler = ReflectionTestUtils.getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
assertThat(ReflectionTestUtils.getField(handler, "requestCache")).isSameAs(requestCache);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,587 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
||||
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.SessionLimit;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
public class SessionManagementSpecTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
this.client = WebTestClient.bindToApplicationContext(context).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionPreventsLoginThenSecondLoginFails() {
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// second login should fail
|
||||
ResponseCookie secondLoginSessionCookie = this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
assertThat(secondLoginSessionCookie).isNull();
|
||||
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpBasicWhenUsingSavingAuthenticationInWebSessionAndPreventLoginThenSecondRequestFails() {
|
||||
this.spring.register(ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
// first request be successful
|
||||
ResponseCookie sessionCookie = this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
// request with no session should fail
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isUnauthorized();
|
||||
|
||||
// request with session obtained from first request should be successful
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.cookie(sessionCookie.getName(), sessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionPerAuthenticationThenUserLoginFailsAndAdminLoginSucceeds() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginConfig.sessionLimit = (authentication) -> {
|
||||
if (authentication.getName().equals("admin")) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(1);
|
||||
};
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
MultiValueMap<String, String> adminCreds = new LinkedMultiValueMap<>();
|
||||
adminCreds.add("username", "admin");
|
||||
adminCreds.add("password", "password");
|
||||
|
||||
ResponseCookie userFirstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie adminFirstLoginSessionCookie = loginReturningCookie(adminCreds);
|
||||
// second user login should fail
|
||||
this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error");
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(userFirstLoginSessionCookie.getName(), userFirstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ResponseCookie adminSecondLoginSessionCookie = loginReturningCookie(adminCreds);
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(adminFirstLoginSessionCookie.getName(), adminFirstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(adminSecondLoginSessionCookie.getName(), adminSecondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(1);
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// first login should not be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
|
||||
// second login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionDoesNotPreventLoginThenLeastRecentlyUsedSessionIsInvalidated() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(2);
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// update last access time for first request
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
ResponseCookie thirdLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// second login should be invalid, it is the least recently used session
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
|
||||
// first login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
// third login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(thirdLoginSessionCookie.getName(), thirdLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void oauth2LoginWhenMaxSessionsThenPreventLogin() {
|
||||
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
|
||||
OAuth2LoginConcurrentSessionsConfig.preventLogin = true;
|
||||
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
|
||||
prepareOAuth2Config();
|
||||
// @formatter:off
|
||||
ResponseCookie sessionCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectHeader().location("/login?error");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(sessionCookie.getName(), sessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("ok");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void oauth2LoginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
|
||||
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
|
||||
OAuth2LoginConcurrentSessionsConfig.preventLogin = false;
|
||||
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
|
||||
prepareOAuth2Config();
|
||||
// @formatter:off
|
||||
ResponseCookie firstLoginCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
ResponseCookie secondLoginCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(firstLoginCookie.getName(), firstLoginCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isFound()
|
||||
.expectHeader().location("/login");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(secondLoginCookie.getName(), secondLoginCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("ok");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenAuthenticationSuccessHandlerOverriddenThenConcurrentSessionHandlersBackOff() {
|
||||
this.spring.register(ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig.class).autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// first login should be successful
|
||||
login(data).expectStatus().isFound().expectHeader().location("/");
|
||||
// second login should be successful, there should be no concurrent session
|
||||
// control
|
||||
login(data).expectStatus().isFound().expectHeader().location("/");
|
||||
}
|
||||
|
||||
private void prepareOAuth2Config() {
|
||||
OAuth2LoginConcurrentSessionsConfig config = this.spring.getContext()
|
||||
.getBean(OAuth2LoginConcurrentSessionsConfig.class);
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
ReactiveAuthenticationManager manager = config.manager;
|
||||
ServerOAuth2AuthorizationRequestResolver resolver = config.resolver;
|
||||
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
|
||||
OAuth2User user = TestOAuth2Users.create();
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
|
||||
OAuth2LoginAuthenticationToken result = new OAuth2LoginAuthenticationToken(
|
||||
TestClientRegistrations.clientRegistration().build(), exchange, user, user.getAuthorities(),
|
||||
accessToken);
|
||||
given(converter.convert(any())).willReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
|
||||
given(manager.authenticate(any())).willReturn(Mono.just(result));
|
||||
given(resolver.resolve(any())).willReturn(Mono.empty());
|
||||
}
|
||||
|
||||
private ResponseCookie loginReturningCookie(MultiValueMap<String, String> data) {
|
||||
return login(data).expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
}
|
||||
|
||||
private WebTestClient.ResponseSpec login(MultiValueMap<String, String> data) {
|
||||
return this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsMaxSessionPreventsLoginConfig {
|
||||
|
||||
static SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(sessionLimit)
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class OAuth2LoginConcurrentSessionsConfig {
|
||||
|
||||
static int maxSessions = 1;
|
||||
|
||||
static boolean preventLogin = true;
|
||||
|
||||
ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class);
|
||||
|
||||
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
|
||||
|
||||
ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http,
|
||||
DefaultWebSessionManager webSessionManager) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.authenticationConverter(this.authenticationConverter)
|
||||
.authenticationManager(this.manager)
|
||||
.authorizationRequestResolver(this.resolver)
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(maxSessions))
|
||||
.maximumSessionsExceededHandler(preventLogin
|
||||
? new PreventLoginServerMaximumSessionsExceededHandler()
|
||||
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.getSessionStore()))
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||
return new InMemoryReactiveClientRegistrationRepository(
|
||||
TestClientRegistrations.clientCredentials().build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
|
||||
|
||||
static SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(sessionLimit)
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin((login) -> login
|
||||
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(1))
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.httpBasic((basic) -> basic
|
||||
.securityContextRepository(new WebSessionServerSecurityContextRepository())
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(1))
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ ReactiveAuthenticationTestConfiguration.class, DefaultController.class })
|
||||
static class Config {
|
||||
|
||||
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
|
||||
DefaultWebSessionManager webSessionManager() {
|
||||
return new DefaultWebSessionManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveSessionRegistry reactiveSessionRegistry() {
|
||||
return new InMemoryReactiveSessionRegistry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class DefaultController {
|
||||
|
||||
@GetMapping("/")
|
||||
String index() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -816,41 +816,4 @@ class AuthorizeHttpRequestsDslTests {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request when ip address does not match then responds with forbidden`() {
|
||||
this.spring.register(HasIpAddressConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(get("/path")
|
||||
.with { request ->
|
||||
request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error")
|
||||
request.apply {
|
||||
dispatcherType = DispatcherType.ERROR
|
||||
}
|
||||
})
|
||||
.andExpect(status().isForbidden)
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
open class HasIpAddressConfig {
|
||||
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, hasIpAddress("10.0.0.0/24"))
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class PathController {
|
||||
@RequestMapping("/path")
|
||||
fun path() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.
|
||||
@ -33,7 +33,6 @@ import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
|
||||
@ -368,50 +367,6 @@ class FormLoginDslTests {
|
||||
verify(exactly = 1) { CustomAuthenticationDetailsSourceConfig.AUTHENTICATION_DETAILS_SOURCE.buildDetails(any()) }
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class CustomUsernameParameterConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
formLogin {
|
||||
usernameParameter = "custom-username"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `form login when custom username parameter then used`() {
|
||||
this.spring.register(CustomUsernameParameterConfig::class.java, UserConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(formLogin().userParameter("custom-username"))
|
||||
.andExpect(authenticated())
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class CustomPasswordParameterConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
formLogin {
|
||||
passwordParameter = "custom-password"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `form login when custom password parameter then used`() {
|
||||
this.spring.register(CustomPasswordParameterConfig::class.java, UserConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(formLogin().passwordParam("custom-password"))
|
||||
.andExpect(authenticated())
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class CustomAuthenticationDetailsSourceConfig {
|
||||
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
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.security.authentication.TestAuthentication
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.core.authority.AuthorityUtils
|
||||
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.MvcResult
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Tests for [Saml2LogoutDsl]
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension::class)
|
||||
class Saml2LogoutDslTests {
|
||||
@JvmField
|
||||
val spring = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
lateinit var mockMvc: MockMvc
|
||||
|
||||
@Test
|
||||
fun `saml2Logout when no relying party registration repository then exception`() {
|
||||
Assertions.assertThatThrownBy { this.spring.register(Saml2LogoutNoRelyingPartyRegistrationRepoConfig::class.java).autowire() }
|
||||
.isInstanceOf(BeanCreationException::class.java)
|
||||
.hasMessageContaining("relyingPartyRegistrationRepository cannot be null")
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun `saml2Logout when defaults and not saml login then default logout`() {
|
||||
this.spring.register(Saml2LogoutDefaultsConfig::class.java).autowire()
|
||||
val user = TestAuthentication.authenticatedUser()
|
||||
val result: MvcResult = this.mockMvc.perform(
|
||||
MockMvcRequestBuilders.post("/logout").with(SecurityMockMvcRequestPostProcessors.authentication(user))
|
||||
.with(SecurityMockMvcRequestPostProcessors.csrf()))
|
||||
.andExpect(MockMvcResultMatchers.status().isFound())
|
||||
.andReturn()
|
||||
val location = result.response.getHeader("Location")
|
||||
Assertions.assertThat(location).isEqualTo("/login?logout")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() {
|
||||
this.spring.register(Saml2LogoutDefaultsConfig::class.java).autowire()
|
||||
val principal = DefaultSaml2AuthenticatedPrincipal("user", emptyMap())
|
||||
principal.relyingPartyRegistrationId = "registration-id"
|
||||
val user = Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER"))
|
||||
val result: MvcResult = this.mockMvc.perform(MockMvcRequestBuilders.post("/logout")
|
||||
.with(SecurityMockMvcRequestPostProcessors.authentication(user))
|
||||
.with(SecurityMockMvcRequestPostProcessors.csrf()))
|
||||
.andExpect(MockMvcResultMatchers.status().isFound())
|
||||
.andReturn()
|
||||
val location = result.response.getHeader("Location")
|
||||
Assertions.assertThat(location).startsWith("https://ap.example.org/logout/saml2/request")
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class Saml2LogoutNoRelyingPartyRegistrationRepoConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
saml2Logout { }
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class Saml2LogoutDefaultsConfig {
|
||||
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
saml2Logout { }
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun registrations(): RelyingPartyRegistrationRepository =
|
||||
InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build())
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.web.server
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseCookie
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration
|
||||
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler
|
||||
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler
|
||||
import org.springframework.security.web.server.authentication.SessionLimit
|
||||
import org.springframework.test.web.reactive.server.WebTestClient
|
||||
import org.springframework.util.LinkedMultiValueMap
|
||||
import org.springframework.util.MultiValueMap
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.reactive.config.EnableWebFlux
|
||||
import org.springframework.web.reactive.function.BodyInserters
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager
|
||||
|
||||
/**
|
||||
* Tests for [ServerSessionManagementDsl]
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension::class)
|
||||
class ServerSessionManagementDslTests {
|
||||
|
||||
@JvmField
|
||||
val spring = SpringTestContext(this)
|
||||
|
||||
private lateinit var client: WebTestClient
|
||||
|
||||
@Autowired
|
||||
fun setup(context: ApplicationContext) {
|
||||
this.client = WebTestClient
|
||||
.bindToApplicationContext(context)
|
||||
.configureClient()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when max sessions prevent login then second login fails`() {
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginTrueConfig::class.java).autowire()
|
||||
|
||||
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
data.add("username", "user")
|
||||
data.add("password", "password")
|
||||
|
||||
val firstLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// second login should fail
|
||||
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error")
|
||||
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when max sessions does not prevent login then seconds login succeeds and first session is invalidated`() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.maxSessions = 1
|
||||
this.spring.register(SessionManagementSpecTests.ConcurrentSessionsMaxSessionPreventsLoginFalseConfig::class.java)
|
||||
.autowire()
|
||||
|
||||
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
data.add("username", "user")
|
||||
data.add("password", "password")
|
||||
|
||||
val firstLoginSessionCookie = loginReturningCookie(data)
|
||||
val secondLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// first login should not be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login")
|
||||
|
||||
// second login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie!!.name, secondLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when max sessions does not prevent login then least recently used session is invalidated`() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.maxSessions = 2
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig::class.java).autowire()
|
||||
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
data.add("username", "user")
|
||||
data.add("password", "password")
|
||||
val firstLoginSessionCookie = loginReturningCookie(data)
|
||||
val secondLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// update last access time for first request
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
val thirdLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// second login should be invalid, it is the least recently used session
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie!!.name, secondLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login")
|
||||
|
||||
// first login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
|
||||
// third login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(thirdLoginSessionCookie!!.name, thirdLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
}
|
||||
|
||||
private fun loginReturningCookie(data: MultiValueMap<String, String>): ResponseCookie? {
|
||||
return login(data).expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void::class.java)
|
||||
.responseCookies
|
||||
.getFirst("SESSION")
|
||||
}
|
||||
|
||||
private fun login(data: MultiValueMap<String, String>): WebTestClient.ResponseSpec {
|
||||
return client.mutateWith(SecurityMockServerConfigurers.csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.location("/")
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config::class)
|
||||
open class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
|
||||
|
||||
companion object {
|
||||
var maxSessions = 1
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun springSecurity(http: ServerHttpSecurity, webSessionManager: DefaultWebSessionManager): SecurityWebFilterChain {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
sessionManagement {
|
||||
sessionConcurrency {
|
||||
maximumSessions = SessionLimit.of(maxSessions)
|
||||
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.sessionStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config::class)
|
||||
open class ConcurrentSessionsMaxSessionPreventsLoginTrueConfig {
|
||||
|
||||
@Bean
|
||||
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
sessionManagement {
|
||||
sessionConcurrency {
|
||||
maximumSessions = SessionLimit.of(1)
|
||||
maximumSessionsExceededHandler =
|
||||
PreventLoginServerMaximumSessionsExceededHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(
|
||||
ReactiveAuthenticationTestConfiguration::class,
|
||||
DefaultController::class
|
||||
)
|
||||
open class Config {
|
||||
|
||||
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
|
||||
open fun webSessionManager(): DefaultWebSessionManager {
|
||||
return DefaultWebSessionManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
|
||||
return InMemoryReactiveSessionRegistry()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
open class DefaultController {
|
||||
|
||||
@GetMapping("/")
|
||||
fun index(): String {
|
||||
return "ok"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -53,7 +53,4 @@
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="jwtBearerAccessTokenResponseClient"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="tokenExchangeAccessTokenResponseClient"/>
|
||||
|
||||
</b:beans>
|
@ -42,21 +42,18 @@
|
||||
factory-method="authorizedClientManagerConsumer"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="authorizationCode"/>
|
||||
factory-method="authorizationCodeAuthorizedClientProvider"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="refreshToken"/>
|
||||
factory-method="refreshTokenAuthorizedClientProvider"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="clientCredentials"/>
|
||||
factory-method="clientCredentialsAuthorizedClientProvider"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="password"/>
|
||||
factory-method="passwordAuthorizedClientProvider"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="jwtBearer"/>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
|
||||
factory-method="tokenExchange"/>
|
||||
factory-method="jwtBearerAuthorizedClientProvider"/>
|
||||
|
||||
</b:beans>
|
@ -6,7 +6,7 @@
|
||||
xmlns:tx="http://www.springframework.org/schema/tx"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
|
||||
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd
|
||||
http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.3.xsd">
|
||||
http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.2.xsd">
|
||||
|
||||
<tx:annotation-driven />
|
||||
|
||||
|
@ -23,20 +23,20 @@
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<relying-party-registrations id="registrations">
|
||||
|
||||
<relying-party-registrations>
|
||||
<relying-party-registration registration-id="one"
|
||||
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
|
||||
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
|
||||
assertion-consumer-service-binding="REDIRECT"
|
||||
asserting-party-id="google"/>
|
||||
|
||||
|
||||
<relying-party-registration registration-id="two"
|
||||
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
|
||||
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
|
||||
assertion-consumer-service-binding="POST"
|
||||
asserting-party-id="simple-saml"/>
|
||||
|
||||
|
||||
<asserting-party asserting-party-id="google" entity-id="https://accounts.google.com/o/saml2/idp/entity-id"
|
||||
want-authn-requests-signed="true"
|
||||
single-sign-on-service-location="https://accounts.google.com/o/saml2/idp/sso-url"
|
||||
@ -48,7 +48,7 @@
|
||||
certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
|
||||
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
|
||||
</asserting-party>
|
||||
|
||||
|
||||
<asserting-party asserting-party-id="simple-saml"
|
||||
entity-id="https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"
|
||||
single-sign-on-service-location="https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php"
|
||||
|
@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2002-2021 the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<relying-party-registrations id="registrations">
|
||||
<relying-party-registration registration-id="one"
|
||||
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
|
||||
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
|
||||
assertion-consumer-service-binding="REDIRECT"
|
||||
asserting-party-id="google">
|
||||
<signing-credential
|
||||
certificate-location="classpath:org/springframework/security/config/saml2/rp-certificate.crt"
|
||||
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
|
||||
</relying-party-registration>
|
||||
<asserting-party asserting-party-id="google" entity-id="https://accounts.google.com/o/saml2/idp/entity-id"
|
||||
want-authn-requests-signed="true"
|
||||
single-sign-on-service-location="https://accounts.google.com/o/saml2/idp/sso-url"
|
||||
single-sign-on-service-binding="POST">
|
||||
<verification-credential
|
||||
certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
|
||||
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
|
||||
<encryption-credential
|
||||
certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
|
||||
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
|
||||
</asserting-party>
|
||||
</relying-party-registrations>
|
||||
|
||||
<b:bean class="org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver">
|
||||
<b:constructor-arg ref="registrations"/>
|
||||
<b:property name="relayStateResolver" ref="relayStateResolver"/>
|
||||
</b:bean>
|
||||
|
||||
<b:bean name="relayStateResolver" class="org.mockito.Mockito" factory-method="mock">
|
||||
<b:constructor-arg value="org.springframework.core.convert.converter.Converter" type="java.lang.Class"/>
|
||||
</b:bean>
|
||||
</b:beans>
|
@ -23,14 +23,14 @@
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<relying-party-registrations id="registrations">
|
||||
|
||||
<relying-party-registrations>
|
||||
<relying-party-registration registration-id="one"
|
||||
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
|
||||
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
|
||||
assertion-consumer-service-binding="REDIRECT"
|
||||
asserting-party-id="google"/>
|
||||
|
||||
|
||||
<asserting-party asserting-party-id="google" entity-id="https://accounts.google.com/o/saml2/idp/entity-id"
|
||||
want-authn-requests-signed="true"
|
||||
single-sign-on-service-location="https://accounts.google.com/o/saml2/idp/sso-url"
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -125,7 +125,7 @@ public interface SecurityExpressionOperations {
|
||||
* given the permission
|
||||
* @param target the target domain object to check permission on
|
||||
* @param permission the permission to check on the domain object (i.e. "read",
|
||||
* "write", etc.).
|
||||
* "write", etc).
|
||||
* @return true if permission is granted to the {@link #getAuthentication()}, else
|
||||
* false
|
||||
*/
|
||||
@ -136,8 +136,8 @@ public interface SecurityExpressionOperations {
|
||||
* object with a given id, type, and permission.
|
||||
* @param targetId the identifier of the domain object to determine access
|
||||
* @param targetType the type (i.e. com.example.domain.Message)
|
||||
* @param permission the permission to check on the domain object (i.e. "read",
|
||||
* "write", etc.)
|
||||
* @param permission the perission to check on the domain object (i.e. "read",
|
||||
* "write", etc)
|
||||
* @return true if permission is granted to the {@link #getAuthentication()}, else
|
||||
* false
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,7 +20,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@ -31,7 +30,6 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -76,69 +74,31 @@ import org.springframework.util.Assert;
|
||||
* your intentions clearer.
|
||||
*
|
||||
* @author Michael Mayr
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);
|
||||
|
||||
/**
|
||||
* Raw hierarchy configuration where each line represents single or multiple level
|
||||
* role chain.
|
||||
*/
|
||||
private String roleHierarchyStringRepresentation = null;
|
||||
|
||||
/**
|
||||
* {@code rolesReachableInOneStepMap} is a Map that under the key of a specific role
|
||||
* name contains a set of all roles reachable from this role in 1 step (i.e. parsed
|
||||
* {@link #roleHierarchyStringRepresentation} grouped by the higher role)
|
||||
*/
|
||||
private Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
|
||||
|
||||
/**
|
||||
* {@code rolesReachableInOneOrMoreStepsMap} is a Map that under the key of a specific
|
||||
* role name contains a set of all roles reachable from this role in 1 or more steps
|
||||
* (i.e. fully resolved hierarchy from {@link #rolesReachableInOneStepMap})
|
||||
*/
|
||||
private Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link RoleHierarchyImpl#fromHierarchy} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public RoleHierarchyImpl() {
|
||||
|
||||
}
|
||||
|
||||
private RoleHierarchyImpl(Map<String, Set<GrantedAuthority>> hierarchy) {
|
||||
this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a role hierarchy instance with the given definition, similar to the
|
||||
* following:
|
||||
*
|
||||
* <pre>
|
||||
* ROLE_A > ROLE_B
|
||||
* ROLE_B > ROLE_AUTHENTICATED
|
||||
* ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED
|
||||
* </pre>
|
||||
* @param hierarchy the role hierarchy to use
|
||||
* @return a {@link RoleHierarchyImpl} that uses the given {@code hierarchy}
|
||||
*/
|
||||
public static RoleHierarchyImpl fromHierarchy(String hierarchy) {
|
||||
return new RoleHierarchyImpl(buildRolesReachableInOneStepMap(hierarchy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that creates a {@link Builder} instance with the default role prefix
|
||||
* "ROLE_"
|
||||
* @return a {@link Builder} instance with the default role prefix "ROLE_"
|
||||
* @since 6.3
|
||||
*/
|
||||
public static Builder withDefaultRolePrefix() {
|
||||
return withRolePrefix("ROLE_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that creates a {@link Builder} instance with the specified role
|
||||
* prefix.
|
||||
* @param rolePrefix the prefix to be used for the roles in the hierarchy.
|
||||
* @return a new {@link Builder} instance with the specified role prefix
|
||||
* @throws IllegalArgumentException if the provided role prefix is null
|
||||
* @since 6.3
|
||||
*/
|
||||
public static Builder withRolePrefix(String rolePrefix) {
|
||||
Assert.notNull(rolePrefix, "rolePrefix must not be null");
|
||||
return new Builder(rolePrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the role hierarchy and pre-calculate for every role the set of all reachable
|
||||
* roles, i.e. all roles lower in the hierarchy of every given role. Pre-calculation
|
||||
@ -146,15 +106,13 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
* time). During pre-calculation, cycles in role hierarchy are detected and will cause
|
||||
* a <tt>CycleInRoleHierarchyException</tt> to be thrown.
|
||||
* @param roleHierarchyStringRepresentation - String definition of the role hierarchy.
|
||||
* @deprecated Use {@link RoleHierarchyImpl#fromHierarchy} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public void setHierarchy(String roleHierarchyStringRepresentation) {
|
||||
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
|
||||
logger.debug(LogMessage.format("setHierarchy() - The following role hierarchy was set: %s",
|
||||
roleHierarchyStringRepresentation));
|
||||
Map<String, Set<GrantedAuthority>> hierarchy = buildRolesReachableInOneStepMap(
|
||||
roleHierarchyStringRepresentation);
|
||||
this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy);
|
||||
buildRolesReachableInOneStepMap();
|
||||
buildRolesReachableInOneOrMoreStepsMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -198,21 +156,21 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
* Parse input and build the map for the roles reachable in one step: the higher role
|
||||
* will become a key that references a set of the reachable lower roles.
|
||||
*/
|
||||
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneStepMap(String hierarchy) {
|
||||
Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = new HashMap<>();
|
||||
for (String line : hierarchy.split("\n")) {
|
||||
private void buildRolesReachableInOneStepMap() {
|
||||
this.rolesReachableInOneStepMap = new HashMap<>();
|
||||
for (String line : this.roleHierarchyStringRepresentation.split("\n")) {
|
||||
// Split on > and trim excessive whitespace
|
||||
String[] roles = line.trim().split("\\s+>\\s+");
|
||||
for (int i = 1; i < roles.length; i++) {
|
||||
String higherRole = roles[i - 1];
|
||||
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
|
||||
Set<GrantedAuthority> rolesReachableInOneStepSet;
|
||||
if (!rolesReachableInOneStepMap.containsKey(higherRole)) {
|
||||
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
|
||||
rolesReachableInOneStepSet = new HashSet<>();
|
||||
rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
|
||||
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
|
||||
}
|
||||
else {
|
||||
rolesReachableInOneStepSet = rolesReachableInOneStepMap.get(higherRole);
|
||||
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
|
||||
}
|
||||
rolesReachableInOneStepSet.add(lowerRole);
|
||||
logger.debug(LogMessage.format(
|
||||
@ -220,7 +178,6 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
higherRole, lowerRole));
|
||||
}
|
||||
}
|
||||
return rolesReachableInOneStepMap;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,105 +186,30 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
* CycleInRoleHierarchyException if a cycle in the role hierarchy definition is
|
||||
* detected)
|
||||
*/
|
||||
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneOrMoreStepsMap(
|
||||
Map<String, Set<GrantedAuthority>> hierarchy) {
|
||||
Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = new HashMap<>();
|
||||
private void buildRolesReachableInOneOrMoreStepsMap() {
|
||||
this.rolesReachableInOneOrMoreStepsMap = new HashMap<>();
|
||||
// iterate over all higher roles from rolesReachableInOneStepMap
|
||||
for (String roleName : hierarchy.keySet()) {
|
||||
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(hierarchy.get(roleName));
|
||||
for (String roleName : this.rolesReachableInOneStepMap.keySet()) {
|
||||
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(roleName));
|
||||
Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
|
||||
while (!rolesToVisitSet.isEmpty()) {
|
||||
// take a role from the rolesToVisit set
|
||||
GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
|
||||
rolesToVisitSet.remove(lowerRole);
|
||||
if (!visitedRolesSet.add(lowerRole) || !hierarchy.containsKey(lowerRole.getAuthority())) {
|
||||
if (!visitedRolesSet.add(lowerRole)
|
||||
|| !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {
|
||||
continue; // Already visited role or role with missing hierarchy
|
||||
}
|
||||
else if (roleName.equals(lowerRole.getAuthority())) {
|
||||
throw new CycleInRoleHierarchyException();
|
||||
}
|
||||
rolesToVisitSet.addAll(hierarchy.get(lowerRole.getAuthority()));
|
||||
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
|
||||
}
|
||||
rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
|
||||
this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
|
||||
logger.debug(LogMessage.format(
|
||||
"buildRolesReachableInOneOrMoreStepsMap() - From role %s one can reach %s in one or more steps.",
|
||||
roleName, visitedRolesSet));
|
||||
}
|
||||
return rolesReachableInOneOrMoreStepsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for constructing a {@link RoleHierarchyImpl} based on a hierarchical
|
||||
* role structure.
|
||||
*
|
||||
* @author Federico Herrera
|
||||
* @since 6.3
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final String rolePrefix;
|
||||
|
||||
private final Map<String, Set<GrantedAuthority>> hierarchy;
|
||||
|
||||
private Builder(String rolePrefix) {
|
||||
this.rolePrefix = rolePrefix;
|
||||
this.hierarchy = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new hierarchy branch to define a role and its child roles.
|
||||
* @param role the highest role in this branch
|
||||
* @return a {@link ImpliedRoles} to define the child roles for the
|
||||
* <code>role</code>
|
||||
*/
|
||||
public ImpliedRoles role(String role) {
|
||||
Assert.hasText(role, "role must not be empty");
|
||||
return new ImpliedRoles(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a {@link RoleHierarchyImpl} describing the defined role
|
||||
* hierarchy.
|
||||
* @return a {@link RoleHierarchyImpl}
|
||||
*/
|
||||
public RoleHierarchyImpl build() {
|
||||
return new RoleHierarchyImpl(this.hierarchy);
|
||||
}
|
||||
|
||||
private Builder addHierarchy(String role, String... impliedRoles) {
|
||||
Set<GrantedAuthority> withPrefix = new HashSet<>();
|
||||
for (String impliedRole : impliedRoles) {
|
||||
withPrefix.add(new SimpleGrantedAuthority(this.rolePrefix.concat(impliedRole)));
|
||||
}
|
||||
this.hierarchy.put(this.rolePrefix.concat(role), withPrefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for constructing child roles within a role hierarchy branch.
|
||||
*/
|
||||
public final class ImpliedRoles {
|
||||
|
||||
private final String role;
|
||||
|
||||
private ImpliedRoles(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies implied role(s) for the current role in the hierarchy.
|
||||
* @param impliedRoles role name(s) implied by the role.
|
||||
* @return the same {@link Builder} instance
|
||||
* @throws IllegalArgumentException if <code>impliedRoles</code> is null,
|
||||
* empty or contains any null element.
|
||||
*/
|
||||
public Builder implies(String... impliedRoles) {
|
||||
Assert.notEmpty(impliedRoles, "at least one implied role must be provided");
|
||||
Assert.noNullElements(impliedRoles, "implied role name(s) cannot be empty");
|
||||
return Builder.this.addHierarchy(this.role, impliedRoles);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user