Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd1b92b7d0 | |||
| 6511cadd2f | |||
| b90ec16fbb | |||
| df3acdad75 | |||
| 7edb123c05 | |||
| 15d9b7824c | |||
| 4131a38f9e | |||
| 5958828113 | |||
| 4491b955b3 | |||
| d2374e3e49 | |||
| aa37574118 | |||
| 602b962af4 | |||
| 0c884005e6 | |||
| 94e0adbea0 | |||
| 65adad8d1e | |||
| 598943e37f | |||
| 1bd6e853ab | |||
| 9be1095deb | |||
| 97516727a4 | |||
| cad6689659 | |||
| fbad708347 | |||
| d8f77e9649 | |||
| eb0460df03 | |||
| e920bfb3a2 | |||
| a1cab162f8 | |||
| d3977d8571 | |||
| 3d53ba62a9 | |||
| 040b879119 | |||
| bd48fdf744 | |||
| 67d3e4c9b8 | |||
| 621ab3e7cb | |||
| 22000b42e9 | |||
| 24abf45128 | |||
| 672d86cbf1 | |||
| b6de302c1b | |||
| 6ab9cc6ff7 | |||
| 9eee9887b9 | |||
| 77acaaa3b7 | |||
| 6e0fb2fc96 | |||
| 51ac8077db | |||
| 85d60580e7 | |||
| f26a7bdb93 | |||
| 09de66f3a9 | |||
| dcd2037dae | |||
| 10cf18db0b | |||
| 9d5194ac52 | |||
| d0a5ada2da | |||
| f501c8547e | |||
| 1589d19c8b | |||
| 318dec845a | |||
| d3ada48190 | |||
| 330c6a4fd5 | |||
| 459dbbc54c | |||
| 84210686d1 | |||
| c84fe6987d | |||
| 1b9553f958 | |||
| 681a85f18b |
@@ -0,0 +1,92 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
milestone: 319 # 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" # 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: "6.0.x"
|
||||
milestone: 143 # 6.0.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" ]
|
||||
@@ -72,6 +72,13 @@ jobs:
|
||||
snapshot_tests:
|
||||
name: Test against snapshots
|
||||
needs: [prerequisites]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- java-version: '21-ea'
|
||||
toolchain: '21'
|
||||
- java-version: '17'
|
||||
toolchain: '17'
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
@@ -79,14 +86,14 @@ jobs:
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: ${{ matrix.java-version }}
|
||||
distribution: 'temurin'
|
||||
- name: Snapshot Tests
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='6.0.+' -PreactorVersion='2022.0.+' -PspringDataVersion='2022.0.+' -PlocksDisabled --stacktrace
|
||||
./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion='6.1.+' -PreactorVersion='2023.0.+' -PspringDataVersion='2023.1.+' -PlocksDisabled --stacktrace
|
||||
check_samples:
|
||||
name: Check Samples project
|
||||
needs: [prerequisites]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Execute Gradle Wrapper Upgrade
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 2am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upgrade_wrapper:
|
||||
name: Execution
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Git configuration
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Upgrade Wrappers
|
||||
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
|
||||
env:
|
||||
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# List of active maintenance branches.
|
||||
branch: [ main, 6.0.x, 5.8.x, 5.7.x ]
|
||||
branch: [ main, 6.1.x, 6.0.x, 5.8.x ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -118,8 +118,7 @@ public class AclPermissionEvaluator implements PermissionEvaluator {
|
||||
if (permission instanceof Permission[]) {
|
||||
return Arrays.asList((Permission[]) permission);
|
||||
}
|
||||
if (permission instanceof String) {
|
||||
String permString = (String) permission;
|
||||
if (permission instanceof String permString) {
|
||||
Permission p = buildPermission(permString);
|
||||
if (p != null) {
|
||||
return Arrays.asList(p);
|
||||
|
||||
@@ -56,10 +56,9 @@ public abstract class AbstractPermission implements Permission {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(obj instanceof Permission)) {
|
||||
if (!(obj instanceof Permission other)) {
|
||||
return false;
|
||||
}
|
||||
Permission other = (Permission) obj;
|
||||
return (this.mask == other.getMask());
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ public class JdbcAclServiceTests {
|
||||
given(this.jdbcOperations.query(anyString(), eq(args), any(RowMapper.class))).willReturn(result);
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
|
||||
List<ObjectIdentity> objectIdentities = this.aclService.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("5577");
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class JdbcAclServiceTests {
|
||||
public void findChildrenWithoutIdType() {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 4711L);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockUntypedIdDomainObject.class.getName());
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(5000L);
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public class JdbcAclServiceTests {
|
||||
public void findChildrenOfIdTypeLong() {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US-PAL");
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(2);
|
||||
assertThat(objectIdentities).hasSize(2);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(4711L);
|
||||
assertThat(objectIdentities.get(1).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
|
||||
@@ -155,7 +155,7 @@ public class JdbcAclServiceTests {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
|
||||
this.aclServiceIntegration.setAclClassIdSupported(true);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("US-PAL");
|
||||
}
|
||||
@@ -165,7 +165,7 @@ public class JdbcAclServiceTests {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockUntypedIdDomainObject.class, 5000L);
|
||||
this.aclServiceIntegration.setAclClassIdSupported(true);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo("costcenter");
|
||||
assertThat(objectIdentities.get(0).getIdentifier())
|
||||
.isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762"));
|
||||
@@ -186,7 +186,7 @@ public class JdbcAclServiceTests {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
|
||||
this.aclServiceIntegration.setAclClassIdSupported(true);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL");
|
||||
}
|
||||
|
||||
+1
-1
@@ -454,7 +454,7 @@ public class JdbcMutableAclServiceTests {
|
||||
CustomSid customSid = new CustomSid("Custom sid");
|
||||
given(customJdbcMutableAclService.createOrRetrieveSidPrimaryKey("Custom sid", false, false)).willReturn(1L);
|
||||
Long result = customJdbcMutableAclService.createOrRetrieveSidPrimaryKey(customSid, false);
|
||||
assertThat(new Long(1L)).isEqualTo(result);
|
||||
assertThat(Long.valueOf(1L)).isEqualTo(result);
|
||||
}
|
||||
|
||||
protected Authentication getAuth() {
|
||||
|
||||
@@ -120,9 +120,9 @@ public class SidTests {
|
||||
PrincipalSid principalSid = new PrincipalSid(authentication);
|
||||
GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_TEST");
|
||||
GrantedAuthoritySid gaSid = new GrantedAuthoritySid(ga);
|
||||
assertThat("johndoe".equals(principalSid.getPrincipal())).isTrue();
|
||||
assertThat("johndoe").isEqualTo(principalSid.getPrincipal());
|
||||
assertThat("scott".equals(principalSid.getPrincipal())).isFalse();
|
||||
assertThat("ROLE_TEST".equals(gaSid.getGrantedAuthority())).isTrue();
|
||||
assertThat("ROLE_TEST").isEqualTo(gaSid.getGrantedAuthority());
|
||||
assertThat("ROLE_TEST2".equals(gaSid.getGrantedAuthority())).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,4 @@ dependencies {
|
||||
testAspect sourceSets.main.output
|
||||
}
|
||||
|
||||
sourceSets.main.aspectj.srcDir "src/main/java"
|
||||
sourceSets.main.java.srcDirs = files()
|
||||
|
||||
sourceSets.test.aspectj.srcDir "src/test/java"
|
||||
sourceSets.test.java.srcDirs = files()
|
||||
|
||||
compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml"
|
||||
|
||||
+2
-2
@@ -143,8 +143,8 @@ public class AnnotationSecurityAspectTests {
|
||||
SecurityContextHolder.getContext().setAuthentication(this.anne);
|
||||
List<String> objects = this.prePostSecured.postFilterMethod();
|
||||
assertThat(objects).hasSize(2);
|
||||
assertThat(objects.contains("apple")).isTrue();
|
||||
assertThat(objects.contains("aubergine")).isTrue();
|
||||
assertThat(objects).contains("apple");
|
||||
assertThat(objects).contains("aubergine");
|
||||
}
|
||||
|
||||
private void configureForElAnnotations() {
|
||||
|
||||
+35
-15
@@ -14,6 +14,10 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.org.gradle.wrapper.upgrade)
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.nohttp'
|
||||
apply plugin: 'locks'
|
||||
apply plugin: 's101'
|
||||
@@ -35,6 +39,7 @@ ext.milestoneBuild = !(snapshotBuild || releaseBuild)
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
}
|
||||
|
||||
tasks.named("saganCreateRelease") {
|
||||
@@ -86,18 +91,30 @@ tasks.named("dispatchGitHubWorkflow") {
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
plugins.withType(JavaPlugin) {
|
||||
java {
|
||||
sourceCompatibility=JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = "UTF-8"
|
||||
options.compilerArgs.add("-parameters")
|
||||
def toolchainVersion() {
|
||||
if (project.hasProperty('testToolchain')) {
|
||||
return project.property('testToolchain').toString().toInteger()
|
||||
}
|
||||
return 17
|
||||
}
|
||||
|
||||
subprojects {
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(toolchainVersion())
|
||||
}
|
||||
}
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
options.compilerArgs.add("-parameters")
|
||||
options.release.set(17)
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
if (!['spring-security-bom', 'spring-security-docs'].contains(project.name)) {
|
||||
@@ -123,12 +140,6 @@ allprojects {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
javaCompiler = javaToolchains.compilerFor {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProperty('buildScan')) {
|
||||
@@ -153,3 +164,12 @@ tasks.register('cloneRepository', IncludeRepoTask) {
|
||||
s101 {
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
wrapperUpgrade {
|
||||
gradle {
|
||||
'spring-security' {
|
||||
repo = 'spring-projects/spring-security'
|
||||
baseBranch = '6.0.x' // runs only on 6.0.x and the update is merged forward to main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ java {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2023 the original author or authors.
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
|
||||
@@ -4,7 +4,6 @@ import org.gradle.api.Action;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
|
||||
@@ -27,12 +26,8 @@ public class AntoraVersionPlugin implements Plugin<Project> {
|
||||
project.getPlugins().withType(LifecycleBasePlugin.class, new Action<LifecycleBasePlugin>() {
|
||||
@Override
|
||||
public void execute(LifecycleBasePlugin lifecycleBasePlugin) {
|
||||
project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME).configure(new Action<Task>() {
|
||||
@Override
|
||||
public void execute(Task check) {
|
||||
check.dependsOn(antoraCheckVersion);
|
||||
}
|
||||
});
|
||||
project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME)
|
||||
.configure(check -> check.dependsOn(antoraCheckVersion));
|
||||
}
|
||||
});
|
||||
project.getTasks().register("antoraUpdateVersion", UpdateAntoraVersionTask.class, new Action<UpdateAntoraVersionTask>() {
|
||||
|
||||
+1
-3
@@ -35,9 +35,7 @@ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin<Pro
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(CheckProhibitedDependenciesLifecyclePlugin.class);
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> {
|
||||
configureProhibitedDependencyChecks(project);
|
||||
});
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> configureProhibitedDependencyChecks(project));
|
||||
}
|
||||
|
||||
private void configureProhibitedDependencyChecks(Project project) {
|
||||
|
||||
+1
-3
@@ -34,8 +34,6 @@ public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin<Projec
|
||||
task.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
|
||||
task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies");
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> {
|
||||
checkTask.dependsOn(checkProhibitedDependencies);
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkProhibitedDependencies));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -26,7 +26,6 @@ import org.gradle.api.Task;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.DependencySet;
|
||||
import org.gradle.api.artifacts.repositories.ExclusiveContentRepository;
|
||||
import org.gradle.api.artifacts.repositories.InclusiveRepositoryContentDescriptor;
|
||||
import org.gradle.api.artifacts.repositories.IvyArtifactRepository;
|
||||
import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
@@ -91,12 +90,7 @@ public class GitHubChangelogPlugin implements Plugin<Project> {
|
||||
@Override
|
||||
public void execute(ExclusiveContentRepository exclusiveContentRepository) {
|
||||
exclusiveContentRepository.forRepositories(repository);
|
||||
exclusiveContentRepository.filter(new Action<InclusiveRepositoryContentDescriptor>() {
|
||||
@Override
|
||||
public void execute(InclusiveRepositoryContentDescriptor descriptor) {
|
||||
descriptor.includeGroup("spring-io");
|
||||
}
|
||||
});
|
||||
exclusiveContentRepository.filter(descriptor -> descriptor.includeGroup("spring-io"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+20
-20
@@ -114,11 +114,11 @@ public final class SpringReleaseTrainSpec {
|
||||
}
|
||||
|
||||
public Builder train(int train) {
|
||||
switch (train) {
|
||||
case 1: this.train = Train.ONE; break;
|
||||
case 2: this.train = Train.TWO; break;
|
||||
default: throw new IllegalArgumentException("Invalid train: " + train);
|
||||
}
|
||||
this.train = switch (train) {
|
||||
case 1 -> Train.ONE;
|
||||
case 2 -> Train.TWO;
|
||||
default -> throw new IllegalArgumentException("Invalid train: " + train);
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -156,13 +156,13 @@ public final class SpringReleaseTrainSpec {
|
||||
}
|
||||
|
||||
public Builder weekOfMonth(int weekOfMonth) {
|
||||
switch (weekOfMonth) {
|
||||
case 1: this.weekOfMonth = WeekOfMonth.FIRST; break;
|
||||
case 2: this.weekOfMonth = WeekOfMonth.SECOND; break;
|
||||
case 3: this.weekOfMonth = WeekOfMonth.THIRD; break;
|
||||
case 4: this.weekOfMonth = WeekOfMonth.FOURTH; break;
|
||||
default: throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth);
|
||||
}
|
||||
this.weekOfMonth = switch (weekOfMonth) {
|
||||
case 1 -> WeekOfMonth.FIRST;
|
||||
case 2 -> WeekOfMonth.SECOND;
|
||||
case 3 -> WeekOfMonth.THIRD;
|
||||
case 4 -> WeekOfMonth.FOURTH;
|
||||
default -> throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth);
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -172,14 +172,14 @@ public final class SpringReleaseTrainSpec {
|
||||
}
|
||||
|
||||
public Builder dayOfWeek(int dayOfWeek) {
|
||||
switch (dayOfWeek) {
|
||||
case 1: this.dayOfWeek = DayOfWeek.MONDAY; break;
|
||||
case 2: this.dayOfWeek = DayOfWeek.TUESDAY; break;
|
||||
case 3: this.dayOfWeek = DayOfWeek.WEDNESDAY; break;
|
||||
case 4: this.dayOfWeek = DayOfWeek.THURSDAY; break;
|
||||
case 5: this.dayOfWeek = DayOfWeek.FRIDAY; break;
|
||||
default: throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek);
|
||||
}
|
||||
this.dayOfWeek = switch (dayOfWeek) {
|
||||
case 1 -> DayOfWeek.MONDAY;
|
||||
case 2 -> DayOfWeek.TUESDAY;
|
||||
case 3 -> DayOfWeek.WEDNESDAY;
|
||||
case 4 -> DayOfWeek.THURSDAY;
|
||||
case 5 -> DayOfWeek.FRIDAY;
|
||||
default -> throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek);
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.publish.Publication;
|
||||
import org.gradle.api.publish.PublishingExtension;
|
||||
import org.gradle.api.publish.plugins.PublishingPlugin;
|
||||
import org.gradle.plugins.signing.SigningExtension;
|
||||
import org.gradle.plugins.signing.SigningPlugin;
|
||||
|
||||
@@ -44,12 +43,7 @@ public class SpringSigningPlugin implements Plugin<Project> {
|
||||
|
||||
private void sign(Project project) {
|
||||
SigningExtension signing = project.getExtensions().findByType(SigningExtension.class);
|
||||
signing.setRequired(new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
return project.getGradle().getTaskGraph().hasTask("publishArtifacts");
|
||||
}
|
||||
});
|
||||
signing.setRequired((Callable<Boolean>) () -> project.getGradle().getTaskGraph().hasTask("publishArtifacts"));
|
||||
String signingKeyId = (String) project.findProperty("signingKeyId");
|
||||
String signingKey = (String) project.findProperty("signingKey");
|
||||
String signingPassword = (String) project.findProperty("signingPassword");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2023 the original author or authors.
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.springframework.security.core.Authentication;
|
||||
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.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
@@ -195,6 +196,9 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
public CasAuthenticationFilter() {
|
||||
super("/login/cas");
|
||||
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
|
||||
@@ -211,9 +215,10 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
}
|
||||
this.logger.debug(
|
||||
LogMessage.format("Authentication success. Updating SecurityContextHolder to contain: %s", authResult));
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
|
||||
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
|
||||
context.setAuthentication(authResult);
|
||||
SecurityContextHolder.setContext(context);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
if (this.eventPublisher != null) {
|
||||
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
|
||||
|
||||
@@ -80,7 +80,6 @@ dependencies {
|
||||
testImplementation "org.hibernate.orm:hibernate-core"
|
||||
testImplementation 'org.hsqldb:hsqldb'
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
testImplementation "org.mockito:mockito-inline"
|
||||
testImplementation('org.seleniumhq.selenium:htmlunit-driver') {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'xml-apis', module: 'xml-apis'
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ public class RSocketMessageHandlerITests {
|
||||
|
||||
@MessageMapping({ "secure.send", "send" })
|
||||
Mono<Void> send(Mono<String> payload) {
|
||||
return payload.doOnNext(this::add).then(Mono.fromRunnable(() -> doNotifyAll()));
|
||||
return payload.doOnNext(this::add).then(Mono.fromRunnable(this::doNotifyAll));
|
||||
}
|
||||
|
||||
private synchronized void doNotifyAll() {
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ public class LdapUserServiceBeanDefinitionParserTests {
|
||||
|
||||
Set<String> authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities());
|
||||
assertThat(authorities).hasSize(3);
|
||||
assertThat(authorities.contains("ROLE_DEVELOPERS")).isTrue();
|
||||
assertThat(authorities).contains("ROLE_DEVELOPERS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,7 +128,7 @@ public class LdapUserServiceBeanDefinitionParserTests {
|
||||
|
||||
Set<String> authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities());
|
||||
assertThat(authorities).hasSize(3);
|
||||
assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue();
|
||||
assertThat(authorities).contains("ROLE_DEVELOPER");
|
||||
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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.1. Please update your schema declarations to the 6.1 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\\.1.*.xsd.*")
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.2.*.xsd.*")
|
||||
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|
||||
|| !schemaLocation.matches("(?m).*spring-security.*");
|
||||
}
|
||||
|
||||
+22
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,7 @@ import java.util.Map;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
@@ -117,7 +118,10 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
|
||||
* @param configurer
|
||||
* @return the {@link SecurityConfigurerAdapter} for further customizations
|
||||
* @throws Exception
|
||||
* @deprecated For removal in 7.0. Use
|
||||
* {@link #with(SecurityConfigurerAdapter, Customizer)} instead.
|
||||
*/
|
||||
@Deprecated(since = "6.2", forRemoval = true)
|
||||
@SuppressWarnings("unchecked")
|
||||
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
|
||||
configurer.addObjectPostProcessor(this.objectPostProcessor);
|
||||
@@ -139,6 +143,23 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
|
||||
return configurer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and
|
||||
* invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
|
||||
* @param configurer
|
||||
* @return the {@link SecurityBuilder} for further customizations
|
||||
* @throws Exception
|
||||
* @since 6.2
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <C extends SecurityConfigurerAdapter<O, B>> B with(C configurer, Customizer<C> customizer) throws Exception {
|
||||
configurer.addObjectPostProcessor(this.objectPostProcessor);
|
||||
configurer.setBuilder((B) this);
|
||||
add(configurer);
|
||||
customizer.customize(configurer);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an object that is shared by multiple {@link SecurityConfigurer}.
|
||||
* @param sharedType the Class to key the shared object by.
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@ final class MethodSecuritySelector implements ImportSelector {
|
||||
|
||||
@Override
|
||||
public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
|
||||
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) {
|
||||
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())
|
||||
&& !importMetadata.hasMetaAnnotation(EnableMethodSecurity.class.getName())) {
|
||||
return new String[0];
|
||||
}
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
|
||||
+11
@@ -70,6 +70,7 @@ import org.springframework.security.config.annotation.web.configurers.SessionMan
|
||||
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
|
||||
@@ -2835,6 +2836,16 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
return HttpSecurity.this;
|
||||
}
|
||||
|
||||
public OidcLogoutConfigurer<HttpSecurity> oidcLogout() throws Exception {
|
||||
return getOrApply(new OidcLogoutConfigurer<>());
|
||||
}
|
||||
|
||||
public HttpSecurity oidcLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oidcLogoutCustomizer)
|
||||
throws Exception {
|
||||
oidcLogoutCustomizer.customize(getOrApply(new OidcLogoutConfigurer<>()));
|
||||
return HttpSecurity.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OAuth 2.0 Client support.
|
||||
* @return the {@link OAuth2ClientConfigurer} for further customizations
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,6 +47,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.web.accept.ContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
@@ -124,10 +125,18 @@ class HttpSecurityConfiguration {
|
||||
.apply(new DefaultLoginPageConfigurer<>());
|
||||
http.logout(withDefaults());
|
||||
// @formatter:on
|
||||
applyCorsIfAvailable(http);
|
||||
applyDefaultConfigurers(http);
|
||||
return http;
|
||||
}
|
||||
|
||||
private void applyCorsIfAvailable(HttpSecurity http) throws Exception {
|
||||
String[] beanNames = this.context.getBeanNamesForType(CorsConfigurationSource.class);
|
||||
if (beanNames.length == 1) {
|
||||
http.cors(withDefaults());
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationManager authenticationManager() throws Exception {
|
||||
return this.authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
+301
-51
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,19 +16,46 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configuration;
|
||||
|
||||
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.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
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.context.annotation.ImportSelector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
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.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
@@ -48,7 +75,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
* @since 5.1
|
||||
* @see OAuth2ImportSelector
|
||||
*/
|
||||
@Import(OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class)
|
||||
@Import({ OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class,
|
||||
OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration.class })
|
||||
final class OAuth2ClientConfiguration {
|
||||
|
||||
private static final boolean webMvcPresent;
|
||||
@@ -65,8 +93,22 @@ final class OAuth2ClientConfiguration {
|
||||
if (!webMvcPresent) {
|
||||
return new String[0];
|
||||
}
|
||||
return new String[] { "org.springframework.security.config.annotation.web.configuration."
|
||||
+ "OAuth2ClientConfiguration.OAuth2ClientWebMvcSecurityConfiguration" };
|
||||
return new String[] {
|
||||
OAuth2ClientConfiguration.class.getName() + ".OAuth2ClientWebMvcSecurityConfiguration" };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 6.2.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class OAuth2AuthorizedClientManagerConfiguration {
|
||||
|
||||
@Bean(name = OAuth2AuthorizedClientManagerRegistrar.BEAN_NAME)
|
||||
OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() {
|
||||
return new OAuth2AuthorizedClientManagerRegistrar();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,16 +116,12 @@ final class OAuth2ClientConfiguration {
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient;
|
||||
|
||||
private OAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy;
|
||||
|
||||
private OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar;
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||
OAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
|
||||
@@ -97,26 +135,6 @@ final class OAuth2ClientConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setClientRegistrationRepository(List<ClientRegistrationRepository> clientRegistrationRepositories) {
|
||||
if (clientRegistrationRepositories.size() == 1) {
|
||||
this.clientRegistrationRepository = clientRegistrationRepositories.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientRepository(List<OAuth2AuthorizedClientRepository> authorizedClientRepositories) {
|
||||
if (authorizedClientRepositories.size() == 1) {
|
||||
this.authorizedClientRepository = authorizedClientRepositories.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAccessTokenResponseClient(
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
|
||||
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientManager(List<OAuth2AuthorizedClientManager> authorizedClientManagers) {
|
||||
if (authorizedClientManagers.size() == 1) {
|
||||
@@ -129,33 +147,265 @@ final class OAuth2ClientConfiguration {
|
||||
this.securityContextHolderStrategy = strategy;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
void setAuthorizedClientManagerRegistrar(
|
||||
OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) {
|
||||
this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
if (this.authorizedClientManager != null) {
|
||||
return this.authorizedClientManager;
|
||||
}
|
||||
OAuth2AuthorizedClientManager authorizedClientManager = null;
|
||||
if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) {
|
||||
if (this.accessTokenResponseClient != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
|
||||
.builder()
|
||||
.authorizationCode()
|
||||
.refreshToken()
|
||||
.clientCredentials((configurer) -> configurer.accessTokenResponseClient(this.accessTokenResponseClient))
|
||||
.password()
|
||||
.build();
|
||||
// @formatter:on
|
||||
DefaultOAuth2AuthorizedClientManager defaultAuthorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
defaultAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
authorizedClientManager = defaultAuthorizedClientManager;
|
||||
return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean
|
||||
* definition, if not already present.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 6.2.0
|
||||
*/
|
||||
static final class OAuth2AuthorizedClientManagerRegistrar
|
||||
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(
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider.class,
|
||||
RefreshTokenOAuth2AuthorizedClientProvider.class,
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider.class,
|
||||
PasswordOAuth2AuthorizedClientProvider.class,
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
|
||||
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0
|
||||
|| getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(OAuth2AuthorizedClientManager.class)
|
||||
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
|
||||
.getBeanDefinition();
|
||||
|
||||
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
|
||||
beanDefinition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = (ListableBeanFactory) beanFactory;
|
||||
}
|
||||
|
||||
OAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() {
|
||||
if (getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
|
||||
return null;
|
||||
}
|
||||
return getAuthorizedClientManager();
|
||||
}
|
||||
|
||||
OAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true);
|
||||
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true);
|
||||
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true)
|
||||
.values();
|
||||
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
|
||||
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
|
||||
}
|
||||
else {
|
||||
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
|
||||
authorizedClientProviders
|
||||
.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders
|
||||
.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
|
||||
OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (jwtBearerAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
|
||||
}
|
||||
else {
|
||||
authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
|
||||
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
|
||||
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
|
||||
}
|
||||
|
||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
|
||||
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class));
|
||||
if (authorizedClientManagerConsumer != null) {
|
||||
authorizedClientManagerConsumer.accept(authorizedClientManager);
|
||||
}
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
|
||||
private boolean hasDelegatingAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
if (authorizedClientProviders.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2RefreshTokenGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2ClientCredentialsGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2PasswordGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
JwtBearerGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
|
||||
authorizedClientProviders);
|
||||
additionalAuthorizedClientProviders
|
||||
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
|
||||
return additionalAuthorizedClientProviders;
|
||||
}
|
||||
|
||||
private <T extends OAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
|
||||
T authorizedClientProvider = null;
|
||||
for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) {
|
||||
if (providerClass.isInstance(current)) {
|
||||
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
|
||||
authorizedClientProvider = providerClass.cast(current);
|
||||
}
|
||||
}
|
||||
return authorizedClientManager;
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private static void assertAuthorizedClientProviderIsNull(
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
if (authorizedClientProvider != null) {
|
||||
// @formatter:off
|
||||
throw new BeanInitializationException(String.format(
|
||||
"Unable to create an %s bean. Expected one bean of type %s, but found multiple. " +
|
||||
"Please consider defining only a single bean of this type, or define an %s bean yourself.",
|
||||
OAuth2AuthorizedClientManager.class.getName(),
|
||||
authorizedClientProvider.getClass().getName(),
|
||||
OAuth2AuthorizedClientManager.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-2
@@ -35,6 +35,7 @@ import org.springframework.security.authorization.SpringAuthorizationEventPublis
|
||||
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.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
|
||||
@@ -62,6 +63,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
|
||||
private final Supplier<RoleHierarchy> roleHierarchy;
|
||||
|
||||
private String rolePrefix = "ROLE_";
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
* @param context the {@link ApplicationContext} to use
|
||||
@@ -76,6 +79,11 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
}
|
||||
this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0)
|
||||
? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy());
|
||||
String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
|
||||
if (grantedAuthorityDefaultsBeanNames.length > 0) {
|
||||
GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class);
|
||||
this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,7 +287,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
* customizations
|
||||
*/
|
||||
public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
|
||||
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasRole(role)));
|
||||
return access(withRoleHierarchy(AuthorityAuthorizationManager
|
||||
.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,7 +300,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
* customizations
|
||||
*/
|
||||
public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) {
|
||||
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(roles)));
|
||||
return access(withRoleHierarchy(
|
||||
AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-3
@@ -40,7 +40,6 @@ import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
|
||||
import org.springframework.security.web.session.InvalidSessionStrategy;
|
||||
@@ -83,7 +82,7 @@ import org.springframework.util.Assert;
|
||||
public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {
|
||||
|
||||
private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
|
||||
private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
|
||||
|
||||
private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
|
||||
|
||||
@@ -105,7 +104,7 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
/**
|
||||
* Specify the {@link CsrfTokenRepository} to use. The default is an
|
||||
* {@link HttpSessionCsrfTokenRepository} wrapped by {@link LazyCsrfTokenRepository}.
|
||||
* {@link HttpSessionCsrfTokenRepository}.
|
||||
* @param csrfTokenRepository the {@link CsrfTokenRepository} to use
|
||||
* @return the {@link CsrfConfigurer} for further customizations
|
||||
*/
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* @param sessionAuthenticationStrategy
|
||||
* @return the {@link SessionManagementConfigurer} for further customizations
|
||||
*/
|
||||
SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
|
||||
public SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
|
||||
SessionAuthenticationStrategy sessionAuthenticationStrategy) {
|
||||
this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy);
|
||||
return this;
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||
|
||||
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.JwtTimestampValidator;
|
||||
|
||||
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
|
||||
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
|
||||
new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
}
|
||||
|
||||
}
|
||||
+18
-1
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
@@ -307,7 +309,22 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (this.accessTokenResponseClient != null) {
|
||||
return this.accessTokenResponseClient;
|
||||
}
|
||||
return new DefaultAuthorizationCodeTokenResponseClient();
|
||||
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2AuthorizationCodeGrantRequest.class);
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(resolvableType);
|
||||
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getBeanOrNull(ResolvableType type) {
|
||||
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
|
||||
if (context != null) {
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length == 1) {
|
||||
return (T) context.getBean(names[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -25,6 +25,8 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
@@ -112,4 +114,13 @@ final class OAuth2ClientConfigurerUtils {
|
||||
return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> OidcSessionRegistry getOidcSessionRegistry(B builder) {
|
||||
OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class);
|
||||
if (sessionRegistry == null) {
|
||||
sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry);
|
||||
}
|
||||
return sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+152
-4
@@ -22,9 +22,18 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.GenericApplicationListenerAdapter;
|
||||
import org.springframework.context.event.SmartApplicationListener;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
@@ -32,9 +41,14 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
||||
import org.springframework.security.context.DelegatingApplicationListener;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.core.session.AbstractSessionEvent;
|
||||
import org.springframework.security.core.session.SessionDestroyedEvent;
|
||||
import org.springframework.security.core.session.SessionIdChangedEvent;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
@@ -42,6 +56,9 @@ import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationC
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
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;
|
||||
@@ -67,7 +84,10 @@ import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
@@ -124,6 +144,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
|
||||
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
|
||||
* login page will be made available</li>
|
||||
* <li>{@link OidcSessionRegistry}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Joe Grandja
|
||||
@@ -202,6 +223,18 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the registry for managing the OIDC client-provider session link
|
||||
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
|
||||
* @return the {@link OAuth2LoginConfigurer} for further configuration
|
||||
* @since 6.2
|
||||
*/
|
||||
public OAuth2LoginConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
|
||||
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
|
||||
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization
|
||||
* Server's Authorization Endpoint.
|
||||
@@ -330,10 +363,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
super.init(http);
|
||||
}
|
||||
}
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient;
|
||||
if (accessTokenResponseClient == null) {
|
||||
accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = getAccessTokenResponseClient();
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = getOAuth2UserService();
|
||||
OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider(
|
||||
accessTokenResponseClient, oauth2UserService);
|
||||
@@ -400,6 +430,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
authenticationFilter
|
||||
.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);
|
||||
}
|
||||
configureOidcSessionRegistry(http);
|
||||
super.configure(http);
|
||||
}
|
||||
|
||||
@@ -442,6 +473,16 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAccessTokenResponseClient() {
|
||||
if (this.tokenEndpointConfig.accessTokenResponseClient != null) {
|
||||
return this.tokenEndpointConfig.accessTokenResponseClient;
|
||||
}
|
||||
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2AuthorizationCodeGrantRequest.class);
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(resolvableType);
|
||||
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
|
||||
private OAuth2UserService<OidcUserRequest, OidcUser> getOidcUserService() {
|
||||
if (this.userInfoEndpointConfig.oidcUserService != null) {
|
||||
return this.userInfoEndpointConfig.oidcUserService;
|
||||
@@ -540,6 +581,29 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return AnyRequestMatcher.INSTANCE;
|
||||
}
|
||||
|
||||
private void configureOidcSessionRegistry(B http) {
|
||||
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
|
||||
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
|
||||
if (sessionConfigurer != null) {
|
||||
OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy();
|
||||
sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry);
|
||||
sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy);
|
||||
}
|
||||
OidcClientSessionEventListener listener = new OidcClientSessionEventListener();
|
||||
listener.setSessionRegistry(sessionRegistry);
|
||||
registerDelegateApplicationListener(listener);
|
||||
}
|
||||
|
||||
private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
|
||||
DelegatingApplicationListener delegating = getBeanOrNull(
|
||||
ResolvableType.forType(DelegatingApplicationListener.class));
|
||||
if (delegating == null) {
|
||||
return;
|
||||
}
|
||||
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
|
||||
delegating.addListener(smartListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for the Authorization Server's Authorization Endpoint.
|
||||
*/
|
||||
@@ -787,4 +851,88 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
}
|
||||
|
||||
private static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> {
|
||||
|
||||
private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class);
|
||||
|
||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(AbstractSessionEvent event) {
|
||||
if (event instanceof SessionDestroyedEvent destroyed) {
|
||||
this.logger.debug("Received SessionDestroyedEvent");
|
||||
this.sessionRegistry.removeSessionInformation(destroyed.getId());
|
||||
return;
|
||||
}
|
||||
if (event instanceof SessionIdChangedEvent changed) {
|
||||
this.logger.debug("Received SessionIdChangedEvent");
|
||||
OidcSessionInformation information = this.sessionRegistry
|
||||
.removeSessionInformation(changed.getOldSessionId());
|
||||
if (information == null) {
|
||||
this.logger
|
||||
.debug("Failed to register new session id since old session id was not found in registry");
|
||||
return;
|
||||
}
|
||||
this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry where OIDC Provider sessions are linked to the Client session.
|
||||
* Defaults to in-memory storage.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onAuthentication(Authentication authentication, HttpServletRequest request,
|
||||
HttpServletResponse response) throws SessionAuthenticationException {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
|
||||
return;
|
||||
}
|
||||
String sessionId = session.getId();
|
||||
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
Map<String, String> headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken())
|
||||
: Collections.emptyMap();
|
||||
OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger
|
||||
.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
|
||||
}
|
||||
this.sessionRegistry.saveSessionInformation(registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* The registration for linking OIDC Provider Session information to the Client's
|
||||
* session. Defaults to in-memory storage.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} implementation that
|
||||
* represents the result of authenticating an OIDC Logout token for the purposes of
|
||||
* performing Back-Channel Logout.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutAuthenticationToken
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
|
||||
* Logout</a>
|
||||
*/
|
||||
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
||||
|
||||
private final OidcLogoutToken logoutToken;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
||||
* @param logoutToken a deserialized, verified OIDC Logout Token
|
||||
*/
|
||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
|
||||
super(Collections.emptyList());
|
||||
this.logoutToken = logoutToken;
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OidcLogoutToken getPrincipal() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OidcLogoutToken getCredentials() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.jwt.BadJwtException;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
|
||||
* deserializing it, verifying its signature, and validating its claims.
|
||||
*
|
||||
* <p>
|
||||
* Intended to be included in a
|
||||
* {@link org.springframework.security.authentication.ProviderManager}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutAuthenticationToken
|
||||
* @see org.springframework.security.authentication.ProviderManager
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
|
||||
* Logout</a>
|
||||
*/
|
||||
final class OidcBackChannelLogoutAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private JwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutAuthenticationProvider}
|
||||
*/
|
||||
OidcBackChannelLogoutAuthenticationProvider() {
|
||||
OidcIdTokenDecoderFactory logoutTokenDecoderFactory = new OidcIdTokenDecoderFactory();
|
||||
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
|
||||
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
if (!(authentication instanceof OidcLogoutAuthenticationToken token)) {
|
||||
return null;
|
||||
}
|
||||
String logoutToken = token.getLogoutToken();
|
||||
ClientRegistration registration = token.getClientRegistration();
|
||||
Jwt jwt = decode(registration, logoutToken);
|
||||
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
|
||||
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
||||
.build();
|
||||
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private Jwt decode(ClientRegistration registration, String token) {
|
||||
JwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration);
|
||||
try {
|
||||
return logoutTokenDecoder.decode(token);
|
||||
}
|
||||
catch (BadJwtException failed) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
throw new OAuth2AuthenticationException(error, failed);
|
||||
}
|
||||
catch (Exception failed) {
|
||||
throw new AuthenticationServiceException(failed.getMessage(), failed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link JwtDecoderFactory} to generate {@link JwtDecoder}s that correspond
|
||||
* to the {@link ClientRegistration} associated with the OIDC logout token.
|
||||
* @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use
|
||||
*/
|
||||
void setLogoutTokenDecoderFactory(JwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory) {
|
||||
Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null");
|
||||
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* A filter for the Client-side OIDC Back-Channel Logout endpoint
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
* Spec</a>
|
||||
*/
|
||||
class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final AuthenticationConverter authenticationConverter;
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutFilter}
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
|
||||
* Logout Token authentication
|
||||
* @param authenticationManager the {@link AuthenticationManager} for authenticating
|
||||
* Logout Tokens
|
||||
*/
|
||||
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
|
||||
AuthenticationManager authenticationManager) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
Authentication token;
|
||||
try {
|
||||
token = this.authenticationConverter.convert(request);
|
||||
}
|
||||
catch (AuthenticationServiceException ex) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
throw ex;
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
handleAuthenticationFailure(response, ex);
|
||||
return;
|
||||
}
|
||||
if (token == null) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
Authentication authentication;
|
||||
try {
|
||||
authentication = this.authenticationManager.authenticate(token);
|
||||
}
|
||||
catch (AuthenticationServiceException ex) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
throw ex;
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
handleAuthenticationFailure(response, ex);
|
||||
return;
|
||||
}
|
||||
this.logoutHandler.logout(request, response, authentication);
|
||||
}
|
||||
|
||||
private void handleAuthenticationFailure(HttpServletResponse response, Exception ex) throws IOException {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
this.errorHttpMessageConverter.write(oauth2Error(ex), null, new ServletServerHttpResponse(response));
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Exception ex) {
|
||||
if (ex instanceof OAuth2AuthenticationException oauth2) {
|
||||
return oauth2.getError();
|
||||
}
|
||||
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
/**
|
||||
* The strategy for expiring all Client sessions indicated by the logout request.
|
||||
* Defaults to {@link OidcBackChannelLogoutHandler}.
|
||||
* @param logoutHandler the {@link LogoutHandler} to use
|
||||
*/
|
||||
void setLogoutHandler(LogoutHandler logoutHandler) {
|
||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@link LogoutHandler} that locates the sessions associated with a given OIDC
|
||||
* Back-Channel Logout Token and invalidates each one.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
* Spec</a>
|
||||
*/
|
||||
final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
private RestOperations restOperations = new RestTemplate();
|
||||
|
||||
private String logoutEndpointName = "/logout";
|
||||
|
||||
private String sessionCookieName = "JSESSIONID";
|
||||
|
||||
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type";
|
||||
this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Iterable<OidcSessionInformation> sessions = this.sessionRegistry.removeSessionInformation(token.getPrincipal());
|
||||
Collection<String> errors = new ArrayList<>();
|
||||
int totalCount = 0;
|
||||
int invalidatedCount = 0;
|
||||
for (OidcSessionInformation session : sessions) {
|
||||
totalCount++;
|
||||
try {
|
||||
eachLogout(request, session);
|
||||
invalidatedCount++;
|
||||
}
|
||||
catch (RestClientException ex) {
|
||||
this.logger.debug("Failed to invalidate session", ex);
|
||||
errors.add(ex.getMessage());
|
||||
this.sessionRegistry.saveSessionInformation(session);
|
||||
}
|
||||
}
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount, totalCount));
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
handleLogoutFailure(response, oauth2Error(errors));
|
||||
}
|
||||
}
|
||||
|
||||
private void eachLogout(HttpServletRequest request, OidcSessionInformation session) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||
headers.add(credential.getKey(), credential.getValue());
|
||||
}
|
||||
String url = request.getRequestURL().toString();
|
||||
String logout = UriComponentsBuilder.fromHttpUrl(url)
|
||||
.replacePath(this.logoutEndpointName)
|
||||
.build()
|
||||
.toUriString();
|
||||
HttpEntity<?> entity = new HttpEntity<>(null, headers);
|
||||
this.restOperations.postForEntity(logout, entity, Object.class);
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Collection<String> errors) {
|
||||
return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors,
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
private void handleLogoutFailure(HttpServletResponse response, OAuth2Error error) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
try {
|
||||
this.errorHttpMessageConverter.write(error, null, new ServletServerHttpResponse(response));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
|
||||
* this class uses
|
||||
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
|
||||
* sessions.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RestOperations} to perform the per-session back-channel logout
|
||||
* @param restOperations the {@link RestOperations} to use
|
||||
*/
|
||||
void setRestOperations(RestOperations restOperations) {
|
||||
Assert.notNull(restOperations, "restOperations cannot be null");
|
||||
this.restOperations = restOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
|
||||
* since that is the default URI for
|
||||
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
||||
* @param logoutUri the URI to use
|
||||
*/
|
||||
void setLogoutUri(String logoutUri) {
|
||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||
this.logoutEndpointName = logoutUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
|
||||
*
|
||||
* <p>
|
||||
* Note that if you are using Spring Session, this likely needs to change to SESSION.
|
||||
* @param sessionCookieName the cookie name to use
|
||||
*/
|
||||
void setSessionCookieName(String sessionCookieName) {
|
||||
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
||||
this.sessionCookieName = sessionCookieName;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
|
||||
* with the OIDC Back-Channel Logout Spec.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutToken
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
|
||||
* Token</a>
|
||||
* @see <a target="blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC
|
||||
* Back-Channel Logout spec</a>
|
||||
*/
|
||||
final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation";
|
||||
|
||||
private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
||||
|
||||
private final String audience;
|
||||
|
||||
private final String issuer;
|
||||
|
||||
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
|
||||
this.audience = clientRegistration.getClientId();
|
||||
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
Collection<OAuth2Error> errors = new ArrayList<>();
|
||||
|
||||
LogoutTokenClaimAccessor logoutClaims = jwt::getClaims;
|
||||
Map<String, Object> events = logoutClaims.getEvents();
|
||||
if (events == null) {
|
||||
errors.add(invalidLogoutToken("events claim must not be null"));
|
||||
}
|
||||
else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) {
|
||||
errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key"));
|
||||
}
|
||||
|
||||
String issuer = logoutClaims.getIssuer().toExternalForm();
|
||||
if (issuer == null) {
|
||||
errors.add(invalidLogoutToken("iss claim must not be null"));
|
||||
}
|
||||
else if (!this.issuer.equals(issuer)) {
|
||||
errors.add(invalidLogoutToken(
|
||||
"iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`"));
|
||||
}
|
||||
|
||||
List<String> audience = logoutClaims.getAudience();
|
||||
if (audience == null) {
|
||||
errors.add(invalidLogoutToken("aud claim must not be null"));
|
||||
}
|
||||
else if (!audience.contains(this.audience)) {
|
||||
errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`"));
|
||||
}
|
||||
|
||||
Instant issuedAt = logoutClaims.getIssuedAt();
|
||||
if (issuedAt == null) {
|
||||
errors.add(invalidLogoutToken("iat claim must not be null"));
|
||||
}
|
||||
|
||||
String jwtId = logoutClaims.getId();
|
||||
if (jwtId == null) {
|
||||
errors.add(invalidLogoutToken("jti claim must not be null"));
|
||||
}
|
||||
|
||||
if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) {
|
||||
errors.add(invalidLogoutToken("sub and sid claims must not both be null"));
|
||||
}
|
||||
|
||||
if (logoutClaims.getClaim("nonce") != null) {
|
||||
errors.add(invalidLogoutToken("nonce claim must not be present"));
|
||||
}
|
||||
|
||||
return OAuth2TokenValidatorResult.failure(errors);
|
||||
}
|
||||
|
||||
private static OAuth2Error invalidLogoutToken(String description) {
|
||||
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL);
|
||||
}
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication
|
||||
* request
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
final class OidcLogoutAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}";
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private RequestMatcher requestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_URI, "POST");
|
||||
|
||||
OidcLogoutAuthenticationConverter(ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
RequestMatcher.MatchResult result = this.requestMatcher.matcher(request);
|
||||
if (!result.isMatch()) {
|
||||
return null;
|
||||
}
|
||||
String registrationId = result.getVariables().get("registrationId");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
|
||||
if (clientRegistration == null) {
|
||||
this.logger.debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found");
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
String logoutToken = request.getParameter("logout_token");
|
||||
if (logoutToken == null) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found");
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration);
|
||||
}
|
||||
|
||||
/**
|
||||
* The logout endpoint. Defaults to
|
||||
* {@code /logout/connect/back-channel/{registrationId}}.
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
|
||||
this.requestMatcher = requestMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} instance that represents a
|
||||
* request to authenticate an OIDC Logout Token.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final String logoutToken;
|
||||
|
||||
private final ClientRegistration clientRegistration;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcLogoutAuthenticationToken}
|
||||
* @param logoutToken a signed, serialized OIDC Logout token
|
||||
* @param clientRegistration the {@link ClientRegistration client} associated with
|
||||
* this token; this is usually derived from material in the logout HTTP request
|
||||
*/
|
||||
OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.logoutToken = logoutToken;
|
||||
this.clientRegistration = clientRegistration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getCredentials() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getPrincipal() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signed, serialized OIDC Logout token
|
||||
* @return the logout token
|
||||
*/
|
||||
String getLogoutToken() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ClientRegistration} associated with this logout token
|
||||
* @return the {@link ClientRegistration}
|
||||
*/
|
||||
ClientRegistration getClientRegistration() {
|
||||
return this.clientRegistration;
|
||||
}
|
||||
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} for OIDC Logout flows
|
||||
*
|
||||
* <p>
|
||||
* OIDC Logout provides an application with the capability to have users log out by using
|
||||
* their existing account at an OAuth 2.0 or OpenID Connect 1.0 Provider.
|
||||
*
|
||||
*
|
||||
* <h2>Security Filters</h2>
|
||||
*
|
||||
* The following {@code Filter} is populated:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link OidcBackChannelLogoutFilter}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Shared Objects Used</h2>
|
||||
*
|
||||
* The following shared objects are used:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link ClientRegistrationRepository}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see HttpSecurity#oidcLogout()
|
||||
* @see OidcBackChannelLogoutFilter
|
||||
* @see ClientRegistrationRepository
|
||||
*/
|
||||
public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
extends AbstractHttpConfigurer<OidcLogoutConfigurer<B>, B> {
|
||||
|
||||
private BackChannelLogoutConfigurer backChannel;
|
||||
|
||||
/**
|
||||
* Sets the repository of client registrations.
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
* @return the {@link OAuth2LoginConfigurer} for further configuration
|
||||
*/
|
||||
public OidcLogoutConfigurer<B> clientRegistrationRepository(
|
||||
ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the registry for managing the OIDC client-provider session link
|
||||
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
|
||||
* @return the {@link OAuth2LoginConfigurer} for further configuration
|
||||
*/
|
||||
public OidcLogoutConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
|
||||
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
|
||||
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
|
||||
* @return the {@link OidcLogoutConfigurer} for further configuration
|
||||
*/
|
||||
public OidcLogoutConfigurer<B> backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
|
||||
if (this.backChannel == null) {
|
||||
this.backChannel = new BackChannelLogoutConfigurer();
|
||||
}
|
||||
backChannelLogoutConfigurer.customize(this.backChannel);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true, since = "6.2")
|
||||
public B and() {
|
||||
return getBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(B builder) throws Exception {
|
||||
if (this.backChannel != null) {
|
||||
this.backChannel.configure(builder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A configurer for configuring OIDC Back-Channel Logout
|
||||
*/
|
||||
public final class BackChannelLogoutConfigurer {
|
||||
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
|
||||
private final AuthenticationManager authenticationManager = new ProviderManager(
|
||||
new OidcBackChannelLogoutAuthenticationProvider());
|
||||
|
||||
private LogoutHandler logoutHandler;
|
||||
|
||||
private AuthenticationConverter authenticationConverter(B http) {
|
||||
if (this.authenticationConverter == null) {
|
||||
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
|
||||
.getClientRegistrationRepository(http);
|
||||
this.authenticationConverter = new OidcLogoutAuthenticationConverter(clientRegistrationRepository);
|
||||
}
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
|
||||
private AuthenticationManager authenticationManager() {
|
||||
return this.authenticationManager;
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(B http) {
|
||||
if (this.logoutHandler == null) {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
return this.logoutHandler;
|
||||
}
|
||||
|
||||
void configure(B http) {
|
||||
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
|
||||
authenticationManager());
|
||||
filter.setLogoutHandler(logoutHandler(http));
|
||||
http.addFilterBefore(filter, CsrfFilter.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+9
-7
@@ -52,6 +52,7 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatchers;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -114,7 +115,9 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
private Saml2AuthenticationRequestResolver authenticationRequestResolver;
|
||||
|
||||
private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
|
||||
private RequestMatcher loginProcessingUrl = RequestMatchers.anyOf(
|
||||
new AntPathRequestMatcher(Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI),
|
||||
new AntPathRequestMatcher("/login/saml2/sso"));
|
||||
|
||||
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
||||
|
||||
@@ -214,7 +217,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
@Override
|
||||
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
|
||||
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
|
||||
this.loginProcessingUrl = loginProcessingUrl;
|
||||
this.loginProcessingUrl = new AntPathRequestMatcher(loginProcessingUrl);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -240,12 +243,11 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
public void init(B http) throws Exception {
|
||||
registerDefaultCsrfOverride(http);
|
||||
relyingPartyRegistrationRepository(http);
|
||||
this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http),
|
||||
this.loginProcessingUrl);
|
||||
this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http));
|
||||
this.saml2WebSsoAuthenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
this.saml2WebSsoAuthenticationFilter.setRequiresAuthenticationRequestMatcher(this.loginProcessingUrl);
|
||||
setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter);
|
||||
setAuthenticationFilter(this.saml2WebSsoAuthenticationFilter);
|
||||
super.loginProcessingUrl(this.loginProcessingUrl);
|
||||
if (StringUtils.hasText(this.loginPage)) {
|
||||
// Set custom login page
|
||||
super.loginPage(this.loginPage);
|
||||
@@ -352,7 +354,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(
|
||||
this.relyingPartyRegistrationRepository);
|
||||
converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
|
||||
converter.setRequestMatcher(createLoginProcessingUrlMatcher(this.loginProcessingUrl));
|
||||
converter.setRequestMatcher(this.loginProcessingUrl);
|
||||
return converter;
|
||||
}
|
||||
return authenticationConverterBean;
|
||||
@@ -370,7 +372,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (csrf == null) {
|
||||
return;
|
||||
}
|
||||
csrf.ignoringRequestMatchers(new AntPathRequestMatcher(this.loginProcessingUrl));
|
||||
csrf.ignoringRequestMatchers(this.loginProcessingUrl);
|
||||
}
|
||||
|
||||
private void initDefaultLoginFilter(B http) {
|
||||
|
||||
+1
-2
@@ -68,8 +68,7 @@ public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProc
|
||||
return;
|
||||
}
|
||||
ConversionService service = beanFactory.getConversionService();
|
||||
if (service instanceof ConverterRegistry) {
|
||||
ConverterRegistry registry = (ConverterRegistry) service;
|
||||
if (service instanceof ConverterRegistry registry) {
|
||||
registry.addConverter(String.class, RSAPrivateKey.class, this.pkcs8);
|
||||
registry.addConverter(String.class, RSAPublicKey.class, this.x509);
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -421,6 +421,10 @@ final class AuthenticationConfigBuilder {
|
||||
this.pc.getReaderContext()
|
||||
.registerWithGeneratedName(new RootBeanDefinition(OAuth2ClientWebMvcSecurityPostProcessor.class));
|
||||
}
|
||||
this.pc.getReaderContext()
|
||||
.getRegistry()
|
||||
.registerBeanDefinition(OAuth2AuthorizedClientManagerRegistrar.BEAN_NAME,
|
||||
new RootBeanDefinition(OAuth2AuthorizedClientManagerRegistrar.class));
|
||||
}
|
||||
|
||||
private void createSaml2LoginFilter(BeanReference authenticationManager,
|
||||
|
||||
+6
-13
@@ -42,19 +42,12 @@ public final class ChannelAttributeFactory {
|
||||
}
|
||||
|
||||
public static List<ConfigAttribute> createChannelAttributes(String requiredChannel) {
|
||||
String channelConfigAttribute;
|
||||
if (requiredChannel.equals(OPT_REQUIRES_HTTPS)) {
|
||||
channelConfigAttribute = "REQUIRES_SECURE_CHANNEL";
|
||||
}
|
||||
else if (requiredChannel.equals(OPT_REQUIRES_HTTP)) {
|
||||
channelConfigAttribute = "REQUIRES_INSECURE_CHANNEL";
|
||||
}
|
||||
else if (requiredChannel.equals(OPT_ANY_CHANNEL)) {
|
||||
channelConfigAttribute = ChannelDecisionManagerImpl.ANY_CHANNEL;
|
||||
}
|
||||
else {
|
||||
throw new BeanCreationException("Unknown channel attribute " + requiredChannel);
|
||||
}
|
||||
String channelConfigAttribute = switch (requiredChannel) {
|
||||
case OPT_REQUIRES_HTTPS -> "REQUIRES_SECURE_CHANNEL";
|
||||
case OPT_REQUIRES_HTTP -> "REQUIRES_INSECURE_CHANNEL";
|
||||
case OPT_ANY_CHANNEL -> ChannelDecisionManagerImpl.ANY_CHANNEL;
|
||||
default -> throw new BeanCreationException("Unknown channel attribute " + requiredChannel);
|
||||
};
|
||||
return SecurityConfig.createList(channelConfigAttribute);
|
||||
}
|
||||
|
||||
|
||||
+7
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,7 +43,6 @@ import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
|
||||
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
|
||||
@@ -109,13 +108,12 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
this.requestHandlerRef = element.getAttribute(ATT_REQUEST_HANDLER);
|
||||
}
|
||||
if (!StringUtils.hasText(this.csrfRepositoryRef)) {
|
||||
RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class);
|
||||
BeanDefinitionBuilder lazyTokenRepository = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(LazyCsrfTokenRepository.class);
|
||||
lazyTokenRepository.addConstructorArgValue(csrfTokenRepository);
|
||||
this.csrfRepositoryRef = pc.getReaderContext().generateBeanName(lazyTokenRepository.getBeanDefinition());
|
||||
pc.registerBeanComponent(
|
||||
new BeanComponentDefinition(lazyTokenRepository.getBeanDefinition(), this.csrfRepositoryRef));
|
||||
BeanDefinitionBuilder httpSessionCsrfTokenRepository = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(HttpSessionCsrfTokenRepository.class);
|
||||
this.csrfRepositoryRef = pc.getReaderContext()
|
||||
.generateBeanName(httpSessionCsrfTokenRepository.getBeanDefinition());
|
||||
pc.registerBeanComponent(new BeanComponentDefinition(httpSessionCsrfTokenRepository.getBeanDefinition(),
|
||||
this.csrfRepositoryRef));
|
||||
}
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class);
|
||||
builder.addConstructorArgReference(this.csrfRepositoryRef);
|
||||
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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.http;
|
||||
|
||||
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.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
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.core.ResolvableType;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
|
||||
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.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.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
|
||||
/**
|
||||
* A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean
|
||||
* definition, if not already present.
|
||||
* <p>
|
||||
* Note: This class is a direct copy of
|
||||
* {@link org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerRegistrar}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 6.2.0
|
||||
*/
|
||||
final class OAuth2AuthorizedClientManagerRegistrar 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(
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider.class,
|
||||
RefreshTokenOAuth2AuthorizedClientProvider.class,
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider.class,
|
||||
PasswordOAuth2AuthorizedClientProvider.class,
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
|
||||
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0
|
||||
|| getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(OAuth2AuthorizedClientManager.class)
|
||||
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
|
||||
.getBeanDefinition();
|
||||
|
||||
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
|
||||
beanDefinition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = (ListableBeanFactory) beanFactory;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true);
|
||||
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true);
|
||||
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true)
|
||||
.values();
|
||||
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
|
||||
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
|
||||
}
|
||||
else {
|
||||
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
|
||||
authorizedClientProviders.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
|
||||
OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (jwtBearerAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
|
||||
}
|
||||
|
||||
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
|
||||
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
|
||||
}
|
||||
|
||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
|
||||
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class));
|
||||
if (authorizedClientManagerConsumer != null) {
|
||||
authorizedClientManagerConsumer.accept(authorizedClientManager);
|
||||
}
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
|
||||
private boolean hasDelegatingAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
if (authorizedClientProviders.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2RefreshTokenGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2ClientCredentialsGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2PasswordGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(ResolvableType
|
||||
.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, JwtBearerGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
|
||||
authorizedClientProviders);
|
||||
additionalAuthorizedClientProviders
|
||||
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
|
||||
return additionalAuthorizedClientProviders;
|
||||
}
|
||||
|
||||
private <T extends OAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
|
||||
T authorizedClientProvider = null;
|
||||
for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) {
|
||||
if (providerClass.isInstance(current)) {
|
||||
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
|
||||
authorizedClientProvider = providerClass.cast(current);
|
||||
}
|
||||
}
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private static void assertAuthorizedClientProviderIsNull(OAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
if (authorizedClientProvider != null) {
|
||||
// @formatter:off
|
||||
throw new BeanInitializationException(String.format(
|
||||
"Unable to create an %s bean. Expected one bean of type %s, but found multiple. " +
|
||||
"Please consider defining only a single bean of this type, or define an %s bean yourself.",
|
||||
OAuth2AuthorizedClientManager.class.getName(),
|
||||
authorizedClientProvider.getClass().getName(),
|
||||
OAuth2AuthorizedClientManager.class.getName()));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
private <T> String[] getBeanNamesForType(Class<T> beanClass) {
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, false, false);
|
||||
}
|
||||
|
||||
private <T> T getBeanOfType(ResolvableType resolvableType) {
|
||||
ObjectProvider<T> objectProvider = this.beanFactory.getBeanProvider(resolvableType, true);
|
||||
return objectProvider.getIfAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
+3
-3
@@ -94,7 +94,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
|
||||
.rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class);
|
||||
String authorizationRequestResolverRef = (authorizationCodeGrantElt != null)
|
||||
? authorizationCodeGrantElt.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF) : null;
|
||||
if (!StringUtils.isEmpty(authorizationRequestResolverRef)) {
|
||||
if (StringUtils.hasLength(authorizationRequestResolverRef)) {
|
||||
authorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef);
|
||||
}
|
||||
else {
|
||||
@@ -125,7 +125,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
|
||||
private BeanMetadataElement getAuthorizationRequestRepository(Element element) {
|
||||
String authorizationRequestRepositoryRef = (element != null)
|
||||
? element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF) : null;
|
||||
if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) {
|
||||
if (StringUtils.hasLength(authorizationRequestRepositoryRef)) {
|
||||
return new RuntimeBeanReference(authorizationRequestRepositoryRef);
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
@@ -147,7 +147,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
|
||||
private BeanMetadataElement getAccessTokenResponseClient(Element element) {
|
||||
String accessTokenResponseClientRef = (element != null)
|
||||
? element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF) : null;
|
||||
if (!StringUtils.isEmpty(accessTokenResponseClientRef)) {
|
||||
if (StringUtils.hasLength(accessTokenResponseClientRef)) {
|
||||
return new RuntimeBeanReference(accessTokenResponseClientRef);
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@ final class OAuth2ClientBeanDefinitionParserUtils {
|
||||
|
||||
static BeanMetadataElement getClientRegistrationRepository(Element element) {
|
||||
String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF);
|
||||
if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) {
|
||||
if (StringUtils.hasLength(clientRegistrationRepositoryRef)) {
|
||||
return new RuntimeBeanReference(clientRegistrationRepositoryRef);
|
||||
}
|
||||
return new RuntimeBeanReference(ClientRegistrationRepository.class);
|
||||
@@ -50,7 +50,7 @@ final class OAuth2ClientBeanDefinitionParserUtils {
|
||||
|
||||
static BeanMetadataElement getAuthorizedClientRepository(Element element) {
|
||||
String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF);
|
||||
if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) {
|
||||
if (StringUtils.hasLength(authorizedClientRepositoryRef)) {
|
||||
return new RuntimeBeanReference(authorizedClientRepositoryRef);
|
||||
}
|
||||
return null;
|
||||
@@ -58,7 +58,7 @@ final class OAuth2ClientBeanDefinitionParserUtils {
|
||||
|
||||
static BeanMetadataElement getAuthorizedClientService(Element element) {
|
||||
String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF);
|
||||
if (!StringUtils.isEmpty(authorizedClientServiceRef)) {
|
||||
if (StringUtils.hasLength(authorizedClientServiceRef)) {
|
||||
return new RuntimeBeanReference(authorizedClientServiceRef);
|
||||
}
|
||||
return null;
|
||||
|
||||
+12
-12
@@ -176,7 +176,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
Object source = parserContext.extractSource(element);
|
||||
String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL);
|
||||
if (!StringUtils.isEmpty(loginProcessingUrl)) {
|
||||
if (StringUtils.hasLength(loginProcessingUrl)) {
|
||||
WebConfigUtils.validateHttpRedirect(loginProcessingUrl, parserContext, source);
|
||||
oauth2LoginAuthenticationFilterBuilder.addConstructorArgValue(loginProcessingUrl);
|
||||
}
|
||||
@@ -189,7 +189,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
.addConstructorArgValue(accessTokenResponseClient)
|
||||
.addConstructorArgValue(oauth2UserService);
|
||||
String userAuthoritiesMapperRef = element.getAttribute(ATT_USER_AUTHORITIES_MAPPER_REF);
|
||||
if (!StringUtils.isEmpty(userAuthoritiesMapperRef)) {
|
||||
if (StringUtils.hasLength(userAuthoritiesMapperRef)) {
|
||||
oauth2LoginAuthenticationProviderBuilder.addPropertyReference("authoritiesMapper",
|
||||
userAuthoritiesMapperRef);
|
||||
}
|
||||
@@ -199,7 +199,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
BeanDefinitionBuilder oauth2AuthorizationRequestRedirectFilterBuilder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class);
|
||||
String authorizationRequestResolverRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF);
|
||||
if (!StringUtils.isEmpty(authorizationRequestResolverRef)) {
|
||||
if (StringUtils.hasLength(authorizationRequestResolverRef)) {
|
||||
oauth2AuthorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef);
|
||||
}
|
||||
else {
|
||||
@@ -212,7 +212,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
this.oauth2AuthorizationRequestRedirectFilter = oauth2AuthorizationRequestRedirectFilterBuilder
|
||||
.getBeanDefinition();
|
||||
String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF);
|
||||
if (!StringUtils.isEmpty(authenticationSuccessHandlerRef)) {
|
||||
if (StringUtils.hasLength(authenticationSuccessHandlerRef)) {
|
||||
oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler",
|
||||
authenticationSuccessHandlerRef);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
successHandlerBuilder.getBeanDefinition());
|
||||
}
|
||||
String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
|
||||
if (!StringUtils.isEmpty(loginPage)) {
|
||||
if (StringUtils.hasLength(loginPage)) {
|
||||
WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source);
|
||||
this.oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class)
|
||||
@@ -245,7 +245,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
}
|
||||
String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF);
|
||||
if (!StringUtils.isEmpty(authenticationFailureHandlerRef)) {
|
||||
if (StringUtils.hasLength(authenticationFailureHandlerRef)) {
|
||||
oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler",
|
||||
authenticationFailureHandlerRef);
|
||||
}
|
||||
@@ -269,7 +269,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
private BeanMetadataElement getAuthorizationRequestRepository(Element element) {
|
||||
String authorizationRequestRepositoryRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF);
|
||||
if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) {
|
||||
if (StringUtils.hasLength(authorizationRequestRepositoryRef)) {
|
||||
return new RuntimeBeanReference(authorizationRequestRepositoryRef);
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
@@ -299,11 +299,11 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
"org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider")
|
||||
.addConstructorArgValue(accessTokenResponseClient)
|
||||
.addConstructorArgValue(oidcUserService);
|
||||
if (!StringUtils.isEmpty(userAuthoritiesMapperRef)) {
|
||||
if (StringUtils.hasLength(userAuthoritiesMapperRef)) {
|
||||
oidcAuthProviderBuilder.addPropertyReference("authoritiesMapper", userAuthoritiesMapperRef);
|
||||
}
|
||||
String jwtDecoderFactoryRef = element.getAttribute(ATT_JWT_DECODER_FACTORY_REF);
|
||||
if (!StringUtils.isEmpty(jwtDecoderFactoryRef)) {
|
||||
if (StringUtils.hasLength(jwtDecoderFactoryRef)) {
|
||||
oidcAuthProviderBuilder.addPropertyReference("jwtDecoderFactory", jwtDecoderFactoryRef);
|
||||
}
|
||||
return oidcAuthProviderBuilder.getBeanDefinition();
|
||||
@@ -311,7 +311,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
private BeanMetadataElement getOidcUserService(Element element) {
|
||||
String oidcUserServiceRef = element.getAttribute(ATT_OIDC_USER_SERVICE_REF);
|
||||
if (!StringUtils.isEmpty(oidcUserServiceRef)) {
|
||||
if (StringUtils.hasLength(oidcUserServiceRef)) {
|
||||
return new RuntimeBeanReference(oidcUserServiceRef);
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
@@ -321,7 +321,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
private BeanMetadataElement getOAuth2UserService(Element element) {
|
||||
String oauth2UserServiceRef = element.getAttribute(ATT_USER_SERVICE_REF);
|
||||
if (!StringUtils.isEmpty(oauth2UserServiceRef)) {
|
||||
if (StringUtils.hasLength(oauth2UserServiceRef)) {
|
||||
return new RuntimeBeanReference(oauth2UserServiceRef);
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
@@ -331,7 +331,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
private BeanMetadataElement getAccessTokenResponseClient(Element element) {
|
||||
String accessTokenResponseClientRef = element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF);
|
||||
if (!StringUtils.isEmpty(accessTokenResponseClientRef)) {
|
||||
if (StringUtils.hasLength(accessTokenResponseClientRef)) {
|
||||
return new RuntimeBeanReference(accessTokenResponseClientRef);
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
|
||||
+6
-6
@@ -166,7 +166,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
||||
|
||||
BeanMetadataElement getAuthenticationManagerResolver(Element element) {
|
||||
String authenticationManagerResolverRef = element.getAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF);
|
||||
if (!StringUtils.isEmpty(authenticationManagerResolverRef)) {
|
||||
if (StringUtils.hasLength(authenticationManagerResolverRef)) {
|
||||
return new RuntimeBeanReference(authenticationManagerResolverRef);
|
||||
}
|
||||
BeanDefinitionBuilder authenticationManagerResolver = BeanDefinitionBuilder
|
||||
@@ -177,7 +177,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
||||
|
||||
BeanMetadataElement getBearerTokenResolver(Element element) {
|
||||
String bearerTokenResolverRef = element.getAttribute(BEARER_TOKEN_RESOLVER_REF);
|
||||
if (StringUtils.isEmpty(bearerTokenResolverRef)) {
|
||||
if (!StringUtils.hasLength(bearerTokenResolverRef)) {
|
||||
return new RootBeanDefinition(DefaultBearerTokenResolver.class);
|
||||
}
|
||||
return new RuntimeBeanReference(bearerTokenResolverRef);
|
||||
@@ -185,7 +185,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
||||
|
||||
BeanMetadataElement getEntryPoint(Element element) {
|
||||
String entryPointRef = element.getAttribute(ENTRY_POINT_REF);
|
||||
if (StringUtils.isEmpty(entryPointRef)) {
|
||||
if (!StringUtils.hasLength(entryPointRef)) {
|
||||
return this.authenticationEntryPoint;
|
||||
}
|
||||
return new RuntimeBeanReference(entryPointRef);
|
||||
@@ -224,7 +224,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
||||
|
||||
Object getDecoder(Element element) {
|
||||
String decoderRef = element.getAttribute(DECODER_REF);
|
||||
if (!StringUtils.isEmpty(decoderRef)) {
|
||||
if (StringUtils.hasLength(decoderRef)) {
|
||||
return new RuntimeBeanReference(decoderRef);
|
||||
}
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder
|
||||
@@ -235,7 +235,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
||||
|
||||
Object getJwtAuthenticationConverter(Element element) {
|
||||
String jwtDecoderRef = element.getAttribute(JWT_AUTHENTICATION_CONVERTER_REF);
|
||||
return (!StringUtils.isEmpty(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef)
|
||||
return (StringUtils.hasLength(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef)
|
||||
: new JwtAuthenticationConverter();
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
||||
|
||||
BeanMetadataElement getIntrospector(Element element) {
|
||||
String introspectorRef = element.getAttribute(INTROSPECTOR_REF);
|
||||
if (!StringUtils.isEmpty(introspectorRef)) {
|
||||
if (StringUtils.hasLength(introspectorRef)) {
|
||||
return new RuntimeBeanReference(introspectorRef);
|
||||
}
|
||||
String introspectionUri = element.getAttribute(INTROSPECTION_URI);
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ public final class ClientRegistrationsBeanDefinitionParser implements BeanDefini
|
||||
if (providers.containsKey(providerId)) {
|
||||
Map<String, String> provider = providers.get(providerId);
|
||||
String issuer = provider.get(ATT_ISSUER_URI);
|
||||
if (!StringUtils.isEmpty(issuer)) {
|
||||
if (StringUtils.hasLength(issuer)) {
|
||||
ClientRegistration.Builder builder = ClientRegistrations.fromIssuerLocation(issuer)
|
||||
.registrationId(registrationId);
|
||||
return getBuilder(parserContext, builder, provider);
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.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.JwtTimestampValidator;
|
||||
|
||||
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
|
||||
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
|
||||
new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} implementation that
|
||||
* represents the result of authenticating an OIDC Logout token for the purposes of
|
||||
* performing Back-Channel Logout.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutAuthenticationToken
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
|
||||
* Logout</a>
|
||||
*/
|
||||
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
||||
|
||||
private final OidcLogoutToken logoutToken;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
||||
* @param logoutToken a deserialized, verified OIDC Logout Token
|
||||
*/
|
||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
|
||||
super(Collections.emptyList());
|
||||
this.logoutToken = logoutToken;
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OidcLogoutToken getPrincipal() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OidcLogoutToken getCredentials() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.jwt.BadJwtException;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
|
||||
* deserializing it, verifying its signature, and validating its claims.
|
||||
*
|
||||
* <p>
|
||||
* Intended to be included in a
|
||||
* {@link org.springframework.security.authentication.ProviderManager}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutAuthenticationToken
|
||||
* @see org.springframework.security.authentication.ProviderManager
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
|
||||
* Logout</a>
|
||||
*/
|
||||
final class OidcBackChannelLogoutReactiveAuthenticationManager implements ReactiveAuthenticationManager {
|
||||
|
||||
private ReactiveJwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager}
|
||||
*/
|
||||
OidcBackChannelLogoutReactiveAuthenticationManager() {
|
||||
ReactiveOidcIdTokenDecoderFactory logoutTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
|
||||
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
|
||||
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Mono<Authentication> authenticate(Authentication authentication) throws AuthenticationException {
|
||||
if (!(authentication instanceof OidcLogoutAuthenticationToken token)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
String logoutToken = token.getLogoutToken();
|
||||
ClientRegistration registration = token.getClientRegistration();
|
||||
return decode(registration, logoutToken)
|
||||
.map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken)
|
||||
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
||||
.build())
|
||||
.map(OidcBackChannelLogoutAuthentication::new);
|
||||
}
|
||||
|
||||
private Mono<Jwt> decode(ClientRegistration registration, String token) {
|
||||
ReactiveJwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration);
|
||||
try {
|
||||
return logoutTokenDecoder.decode(token);
|
||||
}
|
||||
catch (BadJwtException failed) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
return Mono.error(new OAuth2AuthenticationException(error, failed));
|
||||
}
|
||||
catch (Exception failed) {
|
||||
return Mono.error(new AuthenticationServiceException(failed.getMessage(), failed));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link ReactiveJwtDecoderFactory} to generate {@link JwtDecoder}s that
|
||||
* correspond to the {@link ClientRegistration} associated with the OIDC logout token.
|
||||
* @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use
|
||||
*/
|
||||
void setLogoutTokenDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory) {
|
||||
Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null");
|
||||
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
|
||||
* with the OIDC Back-Channel Logout Spec.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see OidcLogoutToken
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
|
||||
* Token</a>
|
||||
* @see <a target="blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC
|
||||
* Back-Channel Logout spec</a>
|
||||
*/
|
||||
final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation";
|
||||
|
||||
private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
||||
|
||||
private final String audience;
|
||||
|
||||
private final String issuer;
|
||||
|
||||
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
|
||||
this.audience = clientRegistration.getClientId();
|
||||
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
Collection<OAuth2Error> errors = new ArrayList<>();
|
||||
|
||||
LogoutTokenClaimAccessor logoutClaims = jwt::getClaims;
|
||||
Map<String, Object> events = logoutClaims.getEvents();
|
||||
if (events == null) {
|
||||
errors.add(invalidLogoutToken("events claim must not be null"));
|
||||
}
|
||||
else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) {
|
||||
errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key"));
|
||||
}
|
||||
|
||||
String issuer = logoutClaims.getIssuer().toExternalForm();
|
||||
if (issuer == null) {
|
||||
errors.add(invalidLogoutToken("iss claim must not be null"));
|
||||
}
|
||||
else if (!this.issuer.equals(issuer)) {
|
||||
errors.add(invalidLogoutToken(
|
||||
"iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`"));
|
||||
}
|
||||
|
||||
List<String> audience = logoutClaims.getAudience();
|
||||
if (audience == null) {
|
||||
errors.add(invalidLogoutToken("aud claim must not be null"));
|
||||
}
|
||||
else if (!audience.contains(this.audience)) {
|
||||
errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`"));
|
||||
}
|
||||
|
||||
Instant issuedAt = logoutClaims.getIssuedAt();
|
||||
if (issuedAt == null) {
|
||||
errors.add(invalidLogoutToken("iat claim must not be null"));
|
||||
}
|
||||
|
||||
String jwtId = logoutClaims.getId();
|
||||
if (jwtId == null) {
|
||||
errors.add(invalidLogoutToken("jti claim must not be null"));
|
||||
}
|
||||
|
||||
if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) {
|
||||
errors.add(invalidLogoutToken("sub and sid claims must not both be null"));
|
||||
}
|
||||
|
||||
if (logoutClaims.getClaim("nonce") != null) {
|
||||
errors.add(invalidLogoutToken("nonce claim must not be present"));
|
||||
}
|
||||
|
||||
return OAuth2TokenValidatorResult.failure(errors);
|
||||
}
|
||||
|
||||
private static OAuth2Error invalidLogoutToken(String description) {
|
||||
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL);
|
||||
}
|
||||
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 java.nio.charset.StandardCharsets;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
/**
|
||||
* A filter for the Client-side OIDC Back-Channel Logout endpoint
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
* Spec</a>
|
||||
*/
|
||||
class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final ServerAuthenticationConverter authenticationConverter;
|
||||
|
||||
private final ReactiveAuthenticationManager authenticationManager;
|
||||
|
||||
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
|
||||
* Logout Token authentication
|
||||
* @param authenticationManager the {@link AuthenticationManager} for authenticating
|
||||
* Logout Tokens
|
||||
*/
|
||||
OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
|
||||
ReactiveAuthenticationManager authenticationManager) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.authenticationConverter.convert(exchange).onErrorResume(AuthenticationException.class, (ex) -> {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
})
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap(this.authenticationManager::authenticate)
|
||||
.onErrorResume(AuthenticationException.class, (ex) -> {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
})
|
||||
.flatMap((authentication) -> {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
return this.logoutHandler.logout(webFilterExchange, authentication);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
OAuth2Error error = oauth2Error(ex);
|
||||
byte[] bytes = String.format("""
|
||||
{
|
||||
"error_code": "%s",
|
||||
"error_description": "%s",
|
||||
"error_uri: "%s"
|
||||
}
|
||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Flux.just(buffer));
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Exception ex) {
|
||||
if (ex instanceof OAuth2AuthenticationException oauth2) {
|
||||
return oauth2.getError();
|
||||
}
|
||||
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
/**
|
||||
* The strategy for expiring all Client sessions indicated by the logout request.
|
||||
* Defaults to {@link OidcBackChannelServerLogoutHandler}.
|
||||
* @param logoutHandler the {@link LogoutHandler} to use
|
||||
*/
|
||||
void setLogoutHandler(ServerLogoutHandler logoutHandler) {
|
||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@link ServerLogoutHandler} that locates the sessions associated with a given OIDC
|
||||
* Back-Channel Logout Token and invalidates each one.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
* Spec</a>
|
||||
*/
|
||||
final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
|
||||
private WebClient web = WebClient.create();
|
||||
|
||||
private String logoutEndpointName = "/logout";
|
||||
|
||||
private String sessionCookieName = "SESSION";
|
||||
|
||||
@Override
|
||||
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
||||
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
||||
return Mono.defer(() -> {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type";
|
||||
this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
|
||||
}
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
AtomicInteger totalCount = new AtomicInteger(0);
|
||||
AtomicInteger invalidatedCount = new AtomicInteger(0);
|
||||
return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> {
|
||||
totalCount.incrementAndGet();
|
||||
return eachLogout(exchange, session).flatMap((response) -> {
|
||||
invalidatedCount.incrementAndGet();
|
||||
return Mono.empty();
|
||||
}).onErrorResume((ex) -> {
|
||||
this.logger.debug("Failed to invalidate session", ex);
|
||||
return this.sessionRegistry.saveSessionInformation(session).then(Mono.just(ex.getMessage()));
|
||||
});
|
||||
}).collectList().flatMap((list) -> {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount.intValue(),
|
||||
totalCount.intValue()));
|
||||
}
|
||||
if (!list.isEmpty()) {
|
||||
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||
headers.add(credential.getKey(), credential.getValue());
|
||||
}
|
||||
String url = exchange.getExchange().getRequest().getURI().toString();
|
||||
String logout = UriComponentsBuilder.fromHttpUrl(url)
|
||||
.replacePath(this.logoutEndpointName)
|
||||
.build()
|
||||
.toUriString();
|
||||
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Collection<?> errors) {
|
||||
return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors,
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
|
||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
byte[] bytes = String.format("""
|
||||
{
|
||||
"error_code": "%s",
|
||||
"error_description": "%s",
|
||||
"error_uri: "%s"
|
||||
}
|
||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Flux.just(buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
|
||||
* this class uses
|
||||
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
|
||||
* sessions.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link WebClient} to perform the per-session back-channel logout
|
||||
* @param web the {@link WebClient} to use
|
||||
*/
|
||||
void setWebClient(WebClient web) {
|
||||
Assert.notNull(web, "web cannot be null");
|
||||
this.web = web;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
|
||||
* since that is the default URI for
|
||||
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
||||
* @param logoutUri the URI to use
|
||||
*/
|
||||
void setLogoutUri(String logoutUri) {
|
||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||
this.logoutEndpointName = logoutUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
|
||||
*
|
||||
* <p>
|
||||
* Note that if you are using Spring Session, this likely needs to change to SESSION.
|
||||
* @param sessionCookieName the cookie name to use
|
||||
*/
|
||||
void setSessionCookieName(String sessionCookieName) {
|
||||
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
||||
this.sessionCookieName = sessionCookieName;
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} instance that represents a
|
||||
* request to authenticate an OIDC Logout Token.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final String logoutToken;
|
||||
|
||||
private final ClientRegistration clientRegistration;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcLogoutAuthenticationToken}
|
||||
* @param logoutToken a signed, serialized OIDC Logout token
|
||||
* @param clientRegistration the {@link ClientRegistration client} associated with
|
||||
* this token; this is usually derived from material in the logout HTTP request
|
||||
*/
|
||||
OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.logoutToken = logoutToken;
|
||||
this.clientRegistration = clientRegistration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getCredentials() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getPrincipal() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signed, serialized OIDC Logout token
|
||||
* @return the logout token
|
||||
*/
|
||||
String getLogoutToken() {
|
||||
return this.logoutToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ClientRegistration} associated with this logout token
|
||||
* @return the {@link ClientRegistration}
|
||||
*/
|
||||
ClientRegistration getClientRegistration() {
|
||||
return this.clientRegistration;
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication
|
||||
* request
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
final class OidcLogoutServerAuthenticationConverter implements ServerAuthenticationConverter {
|
||||
|
||||
private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}";
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private ServerWebExchangeMatcher exchangeMatcher = new PathPatternParserServerWebExchangeMatcher(DEFAULT_LOGOUT_URI,
|
||||
HttpMethod.POST);
|
||||
|
||||
OidcLogoutServerAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||
return this.exchangeMatcher.matches(exchange)
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.flatMap((match) -> {
|
||||
String registrationId = (String) match.getVariables().get("registrationId");
|
||||
return this.clientRegistrationRepository.findByRegistrationId(registrationId)
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
this.logger
|
||||
.debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found");
|
||||
return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}));
|
||||
})
|
||||
.flatMap((clientRegistration) -> exchange.getFormData().map((data) -> {
|
||||
String logoutToken = data.getFirst("logout_token");
|
||||
return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration);
|
||||
}).switchIfEmpty(Mono.error(() -> {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found");
|
||||
return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* The logout endpoint. Defaults to
|
||||
* {@code /logout/connect/back-channel/{registrationId}}.
|
||||
* @param exchangeMatcher the {@link ServerWebExchangeMatcher} to use
|
||||
*/
|
||||
void setExchangeMatcher(ServerWebExchangeMatcher exchangeMatcher) {
|
||||
Assert.notNull(exchangeMatcher, "exchangeMatcher cannot be null");
|
||||
this.exchangeMatcher = exchangeMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
+348
-2
@@ -21,6 +21,7 @@ import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -28,10 +29,13 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
@@ -67,6 +71,9 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
@@ -113,6 +120,7 @@ import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
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;
|
||||
@@ -147,6 +155,7 @@ import org.springframework.security.web.server.context.SecurityContextServerWebE
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
|
||||
import org.springframework.security.web.server.csrf.CsrfToken;
|
||||
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
||||
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
|
||||
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler;
|
||||
@@ -193,8 +202,10 @@ import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
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.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
@@ -295,6 +306,8 @@ public class ServerHttpSecurity {
|
||||
|
||||
private OAuth2ClientSpec client;
|
||||
|
||||
private OidcLogoutSpec oidcLogout;
|
||||
|
||||
private LogoutSpec logout = new LogoutSpec();
|
||||
|
||||
private LoginPageSpec loginPage = new LoginPageSpec();
|
||||
@@ -1093,6 +1106,33 @@ public class ServerHttpSecurity {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OIDC Connect 1.0 Logout support.
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Bean
|
||||
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
* http
|
||||
* // ...
|
||||
* .oidcLogout((logout) -> logout
|
||||
* .backChannel(Customizer.withDefaults())
|
||||
* );
|
||||
* return http.build();
|
||||
* }
|
||||
* </pre>
|
||||
* @param oidcLogoutCustomizer the {@link Customizer} to provide more options for the
|
||||
* {@link OidcLogoutSpec}
|
||||
* @return the {@link ServerHttpSecurity} to customize
|
||||
* @since 6.2
|
||||
*/
|
||||
public ServerHttpSecurity oidcLogout(Customizer<OidcLogoutSpec> oidcLogoutCustomizer) {
|
||||
if (this.oidcLogout == null) {
|
||||
this.oidcLogout = new OidcLogoutSpec();
|
||||
}
|
||||
oidcLogoutCustomizer.customize(this.oidcLogout);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures HTTP Response Headers. The default headers are:
|
||||
*
|
||||
@@ -1537,6 +1577,9 @@ public class ServerHttpSecurity {
|
||||
if (this.resourceServer != null) {
|
||||
this.resourceServer.configure(this);
|
||||
}
|
||||
if (this.oidcLogout != null) {
|
||||
this.oidcLogout.configure(this);
|
||||
}
|
||||
if (this.client != null) {
|
||||
this.client.configure(this);
|
||||
}
|
||||
@@ -3690,6 +3733,8 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerWebExchangeMatcher authenticationMatcher;
|
||||
|
||||
private ReactiveOidcSessionRegistry oidcSessionRegistry;
|
||||
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
@@ -3721,6 +3766,20 @@ public class ServerHttpSecurity {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link ReactiveOidcSessionRegistry} to use when logins use OIDC.
|
||||
* Default is to look the value up as a Bean, or else use an
|
||||
* {@link InMemoryReactiveOidcSessionRegistry}.
|
||||
* @param oidcSessionRegistry the registry to use
|
||||
* @return the {@link OidcLogoutSpec} to customize
|
||||
* @since 6.2
|
||||
*/
|
||||
public OAuth2LoginSpec oidcSessionRegistry(ReactiveOidcSessionRegistry oidcSessionRegistry) {
|
||||
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
|
||||
this.oidcSessionRegistry = oidcSessionRegistry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
|
||||
@@ -3914,8 +3973,9 @@ public class ServerHttpSecurity {
|
||||
oauthRedirectFilter.setRequestCache(http.requestCache.requestCache);
|
||||
|
||||
ReactiveAuthenticationManager manager = getAuthenticationManager();
|
||||
AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager,
|
||||
authorizedClientRepository);
|
||||
ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry();
|
||||
AuthenticationWebFilter authenticationFilter = new OidcSessionRegistryAuthenticationWebFilter(manager,
|
||||
authorizedClientRepository, sessionRegistry);
|
||||
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
|
||||
authenticationFilter
|
||||
.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
|
||||
@@ -3924,6 +3984,8 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
|
||||
setDefaultEntryPoints(http);
|
||||
http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
|
||||
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
|
||||
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
@@ -3968,6 +4030,16 @@ public class ServerHttpSecurity {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
|
||||
}
|
||||
|
||||
private ReactiveOidcSessionRegistry getOidcSessionRegistry() {
|
||||
if (this.oidcSessionRegistry == null) {
|
||||
this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class);
|
||||
}
|
||||
if (this.oidcSessionRegistry == null) {
|
||||
this.oidcSessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
}
|
||||
return this.oidcSessionRegistry;
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandler == null) {
|
||||
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
|
||||
@@ -4084,6 +4156,157 @@ public class ServerHttpSecurity {
|
||||
return new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
|
||||
}
|
||||
|
||||
private static final class OidcSessionRegistryWebFilter implements WebFilter {
|
||||
|
||||
private final ReactiveOidcSessionRegistry oidcSessionRegistry;
|
||||
|
||||
OidcSessionRegistryWebFilter(ReactiveOidcSessionRegistry oidcSessionRegistry) {
|
||||
this.oidcSessionRegistry = oidcSessionRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return chain.filter(new OidcSessionRegistryServerWebExchange(exchange));
|
||||
}
|
||||
|
||||
private final class OidcSessionRegistryServerWebExchange extends ServerWebExchangeDecorator {
|
||||
|
||||
private final Mono<WebSession> sessionMono;
|
||||
|
||||
protected OidcSessionRegistryServerWebExchange(ServerWebExchange delegate) {
|
||||
super(delegate);
|
||||
this.sessionMono = delegate.getSession().map(OidcSessionRegistryWebSession::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> getSession() {
|
||||
return this.sessionMono;
|
||||
}
|
||||
|
||||
private final class OidcSessionRegistryWebSession implements WebSession {
|
||||
|
||||
private final WebSession session;
|
||||
|
||||
OidcSessionRegistryWebSession(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(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
|
||||
.removeSessionInformation(currentId)
|
||||
.flatMap((information) -> {
|
||||
information = information.withSessionId(this.session.getId());
|
||||
return OidcSessionRegistryWebFilter.this.oidcSessionRegistry
|
||||
.saveSessionInformation(information);
|
||||
})));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> invalidate() {
|
||||
String currentId = this.session.getId();
|
||||
return this.session.invalidate()
|
||||
.then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
|
||||
.removeSessionInformation(currentId)
|
||||
.then(Mono.empty())));
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class OidcSessionRegistryAuthenticationWebFilter
|
||||
extends OAuth2LoginAuthenticationWebFilter {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final ReactiveOidcSessionRegistry oidcSessionRegistry;
|
||||
|
||||
OidcSessionRegistryAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager,
|
||||
ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
|
||||
ReactiveOidcSessionRegistry oidcSessionRegistry) {
|
||||
super(authenticationManager, authorizedClientRepository);
|
||||
this.oidcSessionRegistry = oidcSessionRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> onAuthenticationSuccess(Authentication authentication,
|
||||
WebFilterExchange webFilterExchange) {
|
||||
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
|
||||
return super.onAuthenticationSuccess(authentication, webFilterExchange);
|
||||
}
|
||||
return webFilterExchange.getExchange().getSession().doOnNext((session) -> {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(String.format("Linking a provider [%s] session to this client's session",
|
||||
user.getIssuer()));
|
||||
}
|
||||
}).flatMap((session) -> {
|
||||
Mono<CsrfToken> csrfToken = webFilterExchange.getExchange().getAttribute(CsrfToken.class.getName());
|
||||
return (csrfToken != null)
|
||||
? csrfToken.map((token) -> new OidcSessionInformation(session.getId(),
|
||||
Map.of(token.getHeaderName(), token.getToken()), user))
|
||||
: Mono.just(new OidcSessionInformation(session.getId(), Map.of(), user));
|
||||
})
|
||||
.flatMap(this.oidcSessionRegistry::saveSessionInformation)
|
||||
.then(super.onAuthenticationSuccess(authentication, webFilterExchange));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final class OAuth2ClientSpec {
|
||||
@@ -4756,6 +4979,129 @@ public class ServerHttpSecurity {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OIDC 1.0 Logout support
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
public final class OidcLogoutSpec {
|
||||
|
||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private ReactiveOidcSessionRegistry sessionRegistry;
|
||||
|
||||
private BackChannelLogoutConfigurer backChannel;
|
||||
|
||||
/**
|
||||
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look
|
||||
* the value up as a Bean.
|
||||
* @param clientRegistrationRepository the repository to use
|
||||
* @return the {@link OidcLogoutSpec} to customize
|
||||
*/
|
||||
public OidcLogoutSpec clientRegistrationRepository(
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link ReactiveOidcSessionRegistry}. Default is to use the value
|
||||
* from {@link OAuth2LoginSpec#oidcSessionRegistry}, then look the value up as a
|
||||
* Bean, or else use an {@link InMemoryReactiveOidcSessionRegistry}.
|
||||
* @param sessionRegistry the registry to use
|
||||
* @return the {@link OidcLogoutSpec} to customize
|
||||
*/
|
||||
public OidcLogoutSpec oidcSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
|
||||
* @return the {@link OidcLogoutSpec} for further configuration
|
||||
*/
|
||||
public OidcLogoutSpec backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
|
||||
if (this.backChannel == null) {
|
||||
this.backChannel = new OidcLogoutSpec.BackChannelLogoutConfigurer();
|
||||
}
|
||||
backChannelLogoutConfigurer.customize(this.backChannel);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true, since = "6.2")
|
||||
public ServerHttpSecurity and() {
|
||||
return ServerHttpSecurity.this;
|
||||
}
|
||||
|
||||
void configure(ServerHttpSecurity http) {
|
||||
if (this.backChannel != null) {
|
||||
this.backChannel.configure(http);
|
||||
}
|
||||
}
|
||||
|
||||
private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
|
||||
if (this.clientRegistrationRepository == null) {
|
||||
this.clientRegistrationRepository = getBeanOrNull(ReactiveClientRegistrationRepository.class);
|
||||
}
|
||||
return this.clientRegistrationRepository;
|
||||
}
|
||||
|
||||
private ReactiveOidcSessionRegistry getSessionRegistry() {
|
||||
if (this.sessionRegistry == null && ServerHttpSecurity.this.oauth2Login == null) {
|
||||
return new InMemoryReactiveOidcSessionRegistry();
|
||||
}
|
||||
if (this.sessionRegistry == null) {
|
||||
return ServerHttpSecurity.this.oauth2Login.oidcSessionRegistry;
|
||||
}
|
||||
return this.sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* A configurer for configuring OIDC Back-Channel Logout
|
||||
*/
|
||||
public final class BackChannelLogoutConfigurer {
|
||||
|
||||
private ServerAuthenticationConverter authenticationConverter;
|
||||
|
||||
private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager();
|
||||
|
||||
private ServerLogoutHandler logoutHandler;
|
||||
|
||||
private ServerAuthenticationConverter authenticationConverter() {
|
||||
if (this.authenticationConverter == null) {
|
||||
this.authenticationConverter = new OidcLogoutServerAuthenticationConverter(
|
||||
OidcLogoutSpec.this.getClientRegistrationRepository());
|
||||
}
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
|
||||
private ReactiveAuthenticationManager authenticationManager() {
|
||||
return this.authenticationManager;
|
||||
}
|
||||
|
||||
private ServerLogoutHandler logoutHandler() {
|
||||
if (this.logoutHandler == null) {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
return this.logoutHandler;
|
||||
}
|
||||
|
||||
void configure(ServerHttpSecurity http) {
|
||||
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
|
||||
authenticationManager());
|
||||
filter.setLogoutHandler(logoutHandler());
|
||||
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures anonymous authentication
|
||||
*
|
||||
|
||||
+66
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web
|
||||
|
||||
import jakarta.servlet.Filter
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.checkerframework.checker.units.qual.C
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
|
||||
@@ -24,9 +27,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||
import org.springframework.util.ClassUtils
|
||||
import jakarta.servlet.Filter
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
|
||||
/**
|
||||
* Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl].
|
||||
@@ -107,6 +107,36 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
|
||||
return this.http.apply(configurer).apply(configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a [SecurityConfigurerAdapter] to this [HttpSecurity]
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
* http {
|
||||
* with(CustomSecurityConfigurer<HttpSecurity>()) {
|
||||
* customProperty = "..."
|
||||
* }
|
||||
* }
|
||||
* return http.build()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param configurer
|
||||
* the [HttpSecurity] for further customizations
|
||||
* @since 6.2
|
||||
*/
|
||||
fun <C : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? {
|
||||
return this.http.with(configurer, configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows configuring the [HttpSecurity] to only be invoked when matching the
|
||||
* provided pattern.
|
||||
@@ -838,6 +868,38 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
|
||||
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OIDC 1.0 logout support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
* http {
|
||||
* oauth2Login { }
|
||||
* oidcLogout {
|
||||
* backChannel { }
|
||||
* }
|
||||
* }
|
||||
* return http.build()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param oidcLogoutConfiguration custom configuration to configure the
|
||||
* OIDC 1.0 logout support
|
||||
* @see [OidcLogoutDsl]
|
||||
*/
|
||||
fun oidcLogout(oidcLogoutConfiguration: OidcLogoutDsl.() -> Unit) {
|
||||
val oidcLogoutCustomizer = OidcLogoutDsl().apply(oidcLogoutConfiguration).get()
|
||||
this.http.oidcLogout(oidcLogoutCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures Remember Me authentication.
|
||||
*
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.annotation.web
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
|
||||
import org.springframework.security.config.annotation.web.oauth2.login.OidcBackChannelLogoutDsl
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [HttpSecurity] OAuth 1.0 Logout using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
@SecurityMarker
|
||||
class OidcLogoutDsl {
|
||||
var clientRegistrationRepository: ClientRegistrationRepository? = null
|
||||
var oidcSessionRegistry: OidcSessionRegistry? = null
|
||||
|
||||
private var backChannel: ((OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Configures the OIDC 1.0 Back-Channel endpoint.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
* http {
|
||||
* oauth2Login { }
|
||||
* oidcLogout {
|
||||
* backChannel { }
|
||||
* }
|
||||
* }
|
||||
* return http.build()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param backChannelConfig custom configurations to configure the back-channel endpoint
|
||||
* @see [OidcBackChannelLogoutDsl]
|
||||
*/
|
||||
fun backChannel(backChannelConfig: OidcBackChannelLogoutDsl.() -> Unit) {
|
||||
this.backChannel = OidcBackChannelLogoutDsl().apply(backChannelConfig).get()
|
||||
}
|
||||
|
||||
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>) -> Unit {
|
||||
return { oidcLogout ->
|
||||
clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) }
|
||||
oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) }
|
||||
backChannel?.also { oidcLogout.backChannel(backChannel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.annotation.web.oauth2.login
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using
|
||||
* idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
@OAuth2LoginSecurityMarker
|
||||
class OidcBackChannelLogoutDsl {
|
||||
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit {
|
||||
return { backChannel -> }
|
||||
}
|
||||
}
|
||||
+33
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -650,6 +650,38 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
|
||||
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures logout support using an OpenID Connect 1.0 Provider.
|
||||
* A [ReactiveClientRegistrationRepository] is required and must be registered as a Bean or
|
||||
* configured via [ServerOidcLogoutDsl.clientRegistrationRepository].
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebFluxSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
* return http {
|
||||
* oauth2Login { }
|
||||
* oidcLogout {
|
||||
* backChannel { }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param oidcLogoutConfiguration custom configuration to configure the OIDC 1.0 Logout
|
||||
* @see [ServerOidcLogoutDsl]
|
||||
*/
|
||||
fun oidcLogout(oidcLogoutConfiguration: ServerOidcLogoutDsl.() -> Unit) {
|
||||
val oidcLogoutCustomizer = ServerOidcLogoutDsl().apply(oidcLogoutConfiguration).get()
|
||||
this.http.oidcLogout(oidcLogoutCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all configurations to the provided [ServerHttpSecurity]
|
||||
*/
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerOidcBackChannelLogoutDsl {
|
||||
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit {
|
||||
return { backChannel -> }
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 login using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerOidcLogoutDsl {
|
||||
var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null
|
||||
var oidcSessionRegistry: ReactiveOidcSessionRegistry? = null
|
||||
|
||||
private var backChannel: ((ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Enables OIDC 1.0 Back-Channel Logout support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebFluxSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
* return http {
|
||||
* oauth2Login { }
|
||||
* oidcLogout {
|
||||
* backChannel { }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support
|
||||
* @see [ServerOidcBackChannelLogoutDsl]
|
||||
*/
|
||||
fun backChannel(backChannelConfig: ServerOidcBackChannelLogoutDsl.() -> Unit) {
|
||||
this.backChannel = ServerOidcBackChannelLogoutDsl().apply(backChannelConfig).get()
|
||||
}
|
||||
|
||||
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec) -> Unit {
|
||||
return { oidcLogout ->
|
||||
clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) }
|
||||
oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) }
|
||||
backChannel?.also { oidcLogout.backChannel(backChannel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.1.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
|
||||
http\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd
|
||||
@@ -21,7 +22,8 @@ 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.1.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
|
||||
https\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd
|
||||
|
||||
+1346
File diff suppressed because it is too large
Load Diff
+3812
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -17,11 +17,13 @@
|
||||
package org.springframework.security.config.annotation.authentication.configurers.provisioning;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -73,7 +75,7 @@ public class UserDetailsManagerConfigurerTests {
|
||||
.authorities(authority)
|
||||
.build();
|
||||
// @formatter:on
|
||||
assertThat(userDetails.getAuthorities().stream().findFirst().get()).isEqualTo(authority);
|
||||
assertThat((Optional<GrantedAuthority>) userDetails.getAuthorities().stream().findFirst()).contains(authority);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,7 +101,7 @@ public class UserDetailsManagerConfigurerTests {
|
||||
.authorities(Arrays.asList(authority))
|
||||
.build();
|
||||
// @formatter:on
|
||||
assertThat(userDetails.getAuthorities().stream().findFirst().get()).isEqualTo(authority);
|
||||
assertThat((Optional<GrantedAuthority>) userDetails.getAuthorities().stream().findFirst()).contains(authority);
|
||||
}
|
||||
|
||||
private UserDetailsManagerConfigurer<AuthenticationManagerBuilder, InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder>> configurer() {
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
Assert.notNull(this.myUserRepository);
|
||||
Assert.notNull(this.myUserRepository, "myUserRepository cannot be null");
|
||||
return new AuthenticationProvider() {
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -80,7 +80,7 @@ public class EnableAuthorizationManagerReactiveMethodSecurityTests {
|
||||
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
|
||||
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
|
||||
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
|
||||
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
|
||||
+ ".Publisher (for example, a Mono or Flux) or the function must be a Kotlin coroutine in order to support Reactor Context");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.annotation.method.configuration;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* @author Evgeniy Cheban
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@EnableMethodSecurity
|
||||
public @interface EnableCustomMethodSecurity {
|
||||
|
||||
@AliasFor(annotation = EnableMethodSecurity.class, attribute = "proxyTargetClass")
|
||||
boolean proxyTargetClass() default false;
|
||||
|
||||
@AliasFor(annotation = EnableMethodSecurity.class, attribute = "mode")
|
||||
AdviceMode mode() default AdviceMode.PROXY;
|
||||
|
||||
}
|
||||
+1
-1
@@ -78,7 +78,7 @@ public class EnableReactiveMethodSecurityTests {
|
||||
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
|
||||
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
|
||||
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
|
||||
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
|
||||
+ ".Publisher (for example, a Mono or Flux) or the function must be a Kotlin coroutine in order to support Reactor Context");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+26
@@ -95,6 +95,21 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
@Autowired(required = false)
|
||||
BusinessService businessService;
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void customMethodSecurityPreAuthorizeAdminWhenRoleUserThenAccessDeniedException() {
|
||||
this.spring.register(CustomMethodSecurityServiceConfig.class).autowire();
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorizeAdmin)
|
||||
.withMessage("Access Denied");
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void customMethodSecurityPreAuthorizeAdminWhenRoleAdminThenPasses() {
|
||||
this.spring.register(CustomMethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeAdmin();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void preAuthorizeWhenRoleAdminThenAccessDeniedException() {
|
||||
@@ -436,6 +451,17 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCustomMethodSecurity
|
||||
static class CustomMethodSecurityServiceConfig {
|
||||
|
||||
@Bean
|
||||
MethodSecurityService methodSecurityService() {
|
||||
return new MethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
static class MethodSecurityServiceConfig {
|
||||
|
||||
+15
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.SecurityConfigurer;
|
||||
@@ -149,6 +150,19 @@ public class AbstractConfiguredSecurityBuilderTests {
|
||||
assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withWhenConfigurerThenConfigurerAdded() throws Exception {
|
||||
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
|
||||
assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withWhenDuplicateConfigurerAddedThenDuplicateConfigurerRemoved() throws Exception {
|
||||
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
|
||||
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
|
||||
assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
|
||||
}
|
||||
|
||||
private static class ApplyAndRemoveSecurityConfigurer
|
||||
extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
|
||||
|
||||
|
||||
+3
-3
@@ -47,7 +47,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
|
||||
public void requestMatchersWhenPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/path");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
|
||||
public void requestMatchersWhenHttpMethodAndPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
|
||||
public void requestMatchersWhenHttpMethodAndMvcNotPresentThenReturnAntPathMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET);
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
|
||||
+15
-36
@@ -21,7 +21,6 @@ import java.util.List;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.http.HttpServletMapping;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -83,7 +82,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry
|
||||
.requestMatchers(new RegexRequestMatcher("/a.*", HttpMethod.GET.name()));
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(RegexRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -92,7 +91,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry
|
||||
.requestMatchers(new RegexRequestMatcher("/a.*", null));
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(RegexRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -101,7 +100,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry
|
||||
.requestMatchers(new AntPathRequestMatcher("/a.*", HttpMethod.GET.name()));
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -109,7 +108,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
public void antMatchersWhenPatternParamThenReturnAntPathRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(new AntPathRequestMatcher("/a.*"));
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -118,7 +117,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.dispatcherTypeMatchers(HttpMethod.GET,
|
||||
DispatcherType.ASYNC);
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -126,7 +125,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
public void dispatcherMatchersWhenPatternParamThenReturnAntPathRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.dispatcherTypeMatchers(DispatcherType.INCLUDE);
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -134,7 +133,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
public void requestMatchersWhenPatternAndMvcPresentThenReturnMvcRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/path");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -142,7 +141,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
public void requestMatchersWhenHttpMethodAndPatternAndMvcPresentThenReturnMvcRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -150,7 +149,7 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
public void requestMatchersWhenHttpMethodAndMvcPresentThenReturnMvcRequestMatcherType() {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET);
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers.size()).isEqualTo(1);
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
@@ -228,7 +227,6 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenOnlyDispatcherServletThenAllows() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
|
||||
@@ -252,7 +250,6 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenPathBasedNonDispatcherServletThenAllows() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
|
||||
@@ -260,19 +257,10 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/services/*");
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.defaultMapping();
|
||||
}
|
||||
};
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
|
||||
request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping());
|
||||
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
|
||||
request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.path(this, "/services");
|
||||
}
|
||||
};
|
||||
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/services"));
|
||||
request.setServletPath("/services");
|
||||
request.setPathInfo("/endpoint");
|
||||
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
|
||||
@@ -287,21 +275,12 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class);
|
||||
DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant,
|
||||
mvc, servletContext);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.defaultMapping();
|
||||
}
|
||||
};
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
|
||||
request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping());
|
||||
assertThat(requestMatcher.matches(request)).isFalse();
|
||||
verify(mvc).matches(request);
|
||||
verifyNoInteractions(ant);
|
||||
request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.path(this, "/services");
|
||||
}
|
||||
};
|
||||
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/services"));
|
||||
assertThat(requestMatcher.matches(request)).isFalse();
|
||||
verify(ant).matches(request);
|
||||
verifyNoMoreInteractions(mvc);
|
||||
|
||||
+99
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,6 +47,7 @@ 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.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
@@ -63,9 +64,11 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
|
||||
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -73,6 +76,10 @@ import org.springframework.web.accept.ContentNegotiationStrategy;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -85,6 +92,8 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@@ -352,6 +361,40 @@ public class HttpSecurityConfigurationTests {
|
||||
DefaultLogoutPageGeneratingFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenCorsConfigurationSourceThenApplyCors() {
|
||||
this.spring.register(CorsConfigurationSourceConfig.class, DefaultWithFilterChainConfig.class).autowire();
|
||||
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
|
||||
CorsFilter corsFilter = (CorsFilter) filterChain.getFilters()
|
||||
.stream()
|
||||
.filter((f) -> f instanceof CorsFilter)
|
||||
.findFirst()
|
||||
.get();
|
||||
Object configSource = ReflectionTestUtils.getField(corsFilter, "configSource");
|
||||
assertThat(configSource).isInstanceOf(UrlBasedCorsConfigurationSource.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenAddingCustomDslUsingWithThenApplied() throws Exception {
|
||||
this.spring.register(WithCustomDslConfig.class, UserDetailsConfig.class).autowire();
|
||||
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
|
||||
List<Filter> filters = filterChain.getFilters();
|
||||
assertThat(filters).hasAtLeastOneElementOfType(UsernamePasswordAuthenticationFilter.class);
|
||||
this.mockMvc.perform(formLogin()).andExpectAll(redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenCustomDslAddedFromFactoriesAndDisablingUsingWithThenNotApplied() throws Exception {
|
||||
this.springFactoriesLoader
|
||||
.when(() -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader()))
|
||||
.thenReturn(List.of(new WithCustomDsl()));
|
||||
this.spring.register(WithCustomDslDisabledConfig.class, UserDetailsConfig.class).autowire();
|
||||
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
|
||||
List<Filter> filters = filterChain.getFilters();
|
||||
assertThat(filters).doesNotHaveAnyElementsOfTypes(UsernamePasswordAuthenticationFilter.class);
|
||||
this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class NameController {
|
||||
|
||||
@@ -616,6 +659,20 @@ public class HttpSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CorsConfigurationSourceConfig {
|
||||
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(List.of("http://localhost:8080"));
|
||||
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DefaultConfigurer extends AbstractHttpConfigurer<DefaultConfigurer, HttpSecurity> {
|
||||
|
||||
boolean init;
|
||||
@@ -634,4 +691,45 @@ public class HttpSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class WithCustomDslConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.with(new WithCustomDsl(), Customizer.withDefaults())
|
||||
.httpBasic(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class WithCustomDslDisabledConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.with(new WithCustomDsl(), (dsl) -> dsl.disable())
|
||||
.httpBasic(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class WithCustomDsl extends AbstractHttpConfigurer<WithCustomDsl, HttpSecurity> {
|
||||
|
||||
@Override
|
||||
public void init(HttpSecurity builder) throws Exception {
|
||||
builder.formLogin(Customizer.withDefaults());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+547
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* 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.annotation.web.configuration;
|
||||
|
||||
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 jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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;
|
||||
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
|
||||
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.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
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.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.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.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;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OAuth2AuthorizedClientManagerConfigurationTests {
|
||||
|
||||
private static OAuth2AccessTokenResponseClient<? super AbstractOAuth2AuthorizationGrantRequest> MOCK_RESPONSE_CLIENT;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
@Autowired
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
private AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider;
|
||||
|
||||
private MockHttpServletRequest request;
|
||||
|
||||
private MockHttpServletResponse response;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
MOCK_RESPONSE_CLIENT = mock(OAuth2AccessTokenResponseClient.class);
|
||||
this.request = new MockHttpServletRequest();
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@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);
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId("google")
|
||||
.principal(authentication)
|
||||
.attribute(HttpServletRequest.class.getName(), this.request)
|
||||
.attribute(HttpServletResponse.class.getName(), this.response)
|
||||
.build();
|
||||
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
|
||||
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest))
|
||||
.extracting(OAuth2AuthorizationException::getError)
|
||||
.extracting(OAuth2Error::getErrorCode)
|
||||
.isEqualTo("client_authorization_required");
|
||||
// @formatter:on
|
||||
|
||||
verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class));
|
||||
}
|
||||
|
||||
@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(accessTokenResponse);
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null);
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google");
|
||||
OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
|
||||
authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken());
|
||||
this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.request,
|
||||
this.response);
|
||||
// @formatter:off
|
||||
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
|
||||
.withAuthorizedClient(existingAuthorizedClient)
|
||||
.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<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(accessTokenResponse);
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null);
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github");
|
||||
// @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<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(accessTokenResponse);
|
||||
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook");
|
||||
// @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
|
||||
this.request.setParameter(OAuth2ParameterNames.USERNAME, "user");
|
||||
this.request.setParameter(OAuth2ParameterNames.PASSWORD, "password");
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
|
||||
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(accessTokenResponse);
|
||||
|
||||
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta");
|
||||
// @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<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");
|
||||
}
|
||||
|
||||
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
|
||||
@EnableWebSecurity
|
||||
static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CustomAccessTokenResponseClientsConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
|
||||
return new MockAuthorizationCodeClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient() {
|
||||
return new MockRefreshTokenClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
|
||||
return new MockClientCredentialsClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient() {
|
||||
return new MockPasswordClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerTokenResponseClient() {
|
||||
return new MockJwtBearerClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
|
||||
return mock(DefaultOAuth2UserService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
|
||||
return mock(OidcUserService.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CustomAuthorizedClientProvidersConfig extends OAuth2ClientBaseConfig {
|
||||
|
||||
@Bean
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeProvider() {
|
||||
return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider());
|
||||
}
|
||||
|
||||
@Bean
|
||||
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordOAuth2AuthorizedClientProvider passwordProvider() {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient());
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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(Arrays.asList(
|
||||
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()));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return mock(OAuth2AuthorizedClientRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer() {
|
||||
return (authorizedClientManager) -> authorizedClientManager
|
||||
.setContextAttributesMapper((authorizeRequest) -> {
|
||||
HttpServletRequest request = Objects
|
||||
.requireNonNull(authorizeRequest.getAttribute(HttpServletRequest.class.getName()));
|
||||
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
|
||||
String password = request.getParameter(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 MockAuthorizationCodeClient
|
||||
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-2
@@ -180,9 +180,10 @@ public class OAuth2ClientConfigurationTests {
|
||||
@Test
|
||||
public void loadContextWhenAccessTokenResponseClientRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() {
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(Exception.class)
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> this.spring.register(AccessTokenResponseClientRegisteredTwiceConfig.class).autowire())
|
||||
.withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class)
|
||||
.havingRootCause()
|
||||
.isInstanceOf(NoUniqueBeanDefinitionException.class)
|
||||
.withMessageContaining(
|
||||
"expected single matching bean but found 2: accessTokenResponseClient1,accessTokenResponseClient2");
|
||||
// @formatter:on
|
||||
|
||||
+62
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,14 +17,20 @@
|
||||
package org.springframework.security.config.annotation.web.configuration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnJre;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
@@ -35,6 +41,8 @@ import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
@@ -46,6 +54,7 @@ import org.springframework.security.config.annotation.web.configuration.Security
|
||||
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.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction;
|
||||
@@ -271,6 +280,58 @@ public class SecurityReactorContextConfigurationTests {
|
||||
verify(strategy, times(2)).getContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPublisherWhenThreadFactoryIsPlatformThenSecurityContextAttributesAvailable() throws Exception {
|
||||
this.spring.register(SecurityConfig.class).autowire();
|
||||
|
||||
ThreadFactory threadFactory = Executors.defaultThreadFactory();
|
||||
assertContextAttributesAvailable(threadFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnJre(JRE.JAVA_17)
|
||||
public void createPublisherWhenThreadFactoryIsVirtualThenSecurityContextAttributesAvailable() throws Exception {
|
||||
this.spring.register(SecurityConfig.class).autowire();
|
||||
|
||||
ThreadFactory threadFactory = new VirtualThreadTaskExecutor().getVirtualThreadFactory();
|
||||
assertContextAttributesAvailable(threadFactory);
|
||||
}
|
||||
|
||||
private void assertContextAttributesAvailable(ThreadFactory threadFactory) throws Exception {
|
||||
Map<Object, Object> expectedContextAttributes = new HashMap<>();
|
||||
expectedContextAttributes.put(HttpServletRequest.class, this.servletRequest);
|
||||
expectedContextAttributes.put(HttpServletResponse.class, this.servletResponse);
|
||||
expectedContextAttributes.put(Authentication.class, this.authentication);
|
||||
|
||||
try (SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(threadFactory)) {
|
||||
Future<Map<Object, Object>> future = taskExecutor.submit(this::propagateRequestAttributes);
|
||||
assertThat(future.get()).isEqualTo(expectedContextAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Object, Object> propagateRequestAttributes() {
|
||||
RequestAttributes requestAttributes = new ServletRequestAttributes(this.servletRequest, this.servletResponse);
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(this.authentication);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
// @formatter:off
|
||||
return Mono.deferContextual(Mono::just)
|
||||
.filter((ctx) -> ctx.hasKey(SecurityReactorContextSubscriber.SECURITY_CONTEXT_ATTRIBUTES))
|
||||
.map((ctx) -> ctx.<Map<Object, Object>>get(SecurityReactorContextSubscriber.SECURITY_CONTEXT_ATTRIBUTES))
|
||||
.map((attributes) -> {
|
||||
Map<Object, Object> map = new HashMap<>();
|
||||
// Copy over items from lazily loaded map
|
||||
Arrays.asList(HttpServletRequest.class, HttpServletResponse.class, Authentication.class)
|
||||
.forEach((key) -> map.put(key, attributes.get(key)));
|
||||
return map;
|
||||
})
|
||||
.block();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class SecurityConfig {
|
||||
|
||||
+70
@@ -37,6 +37,7 @@ import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
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.authority.AuthorityUtils;
|
||||
@@ -476,6 +477,43 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(requestWithRoleOther).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenCustomRolePrefixAndRoleHasDifferentPrefixThenRespondsWithForbidden() throws Exception {
|
||||
this.spring.register(GrantedAuthorityDefaultHasRoleConfig.class, BasicController.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/")
|
||||
.with(user("user")
|
||||
.authorities(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
// @formatter:on
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenCustomRolePrefixAndHasRoleThenRespondsWithOk() throws Exception {
|
||||
this.spring.register(GrantedAuthorityDefaultHasRoleConfig.class, BasicController.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/")
|
||||
.with(user("user")
|
||||
.authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_USER")));
|
||||
// @formatter:on
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenCustomRolePrefixAndHasAnyRoleThenRespondsWithOk() throws Exception {
|
||||
this.spring.register(GrantedAuthorityDefaultHasAnyRoleConfig.class, BasicController.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/")
|
||||
.with(user("user")
|
||||
.authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_USER")));
|
||||
MockHttpServletRequestBuilder requestWithAdmin = get("/")
|
||||
.with(user("user")
|
||||
.authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_ADMIN")));
|
||||
// @formatter:on
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isOk());
|
||||
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk()
|
||||
throws Exception {
|
||||
@@ -558,6 +596,38 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class GrantedAuthorityDefaultHasRoleConfig {
|
||||
|
||||
@Bean
|
||||
GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("CUSTOM_PREFIX_");
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain myFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.authorizeHttpRequests((c) -> c.anyRequest().hasRole("USER")).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class GrantedAuthorityDefaultHasAnyRoleConfig {
|
||||
|
||||
@Bean
|
||||
GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("CUSTOM_PREFIX_");
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain myFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.authorizeHttpRequests((c) -> c.anyRequest().hasAnyRole("USER", "ADMIN")).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class NoRequestsConfig {
|
||||
|
||||
+14
-14
@@ -85,10 +85,10 @@ public class DefaultFiltersTests {
|
||||
List<SecurityFilterChain> filterChains = this.spring.getContext()
|
||||
.getBean(FilterChainProxy.class)
|
||||
.getFilterChains();
|
||||
assertThat(filterChains.size()).isEqualTo(1);
|
||||
assertThat(filterChains).hasSize(1);
|
||||
DefaultSecurityFilterChain filterChain = (DefaultSecurityFilterChain) filterChains.get(0);
|
||||
assertThat(filterChain.getRequestMatcher()).isInstanceOf(AnyRequestMatcher.class);
|
||||
assertThat(filterChain.getFilters().size()).isEqualTo(1);
|
||||
assertThat(filterChain.getFilters()).hasSize(1);
|
||||
long filter = filterChain.getFilters()
|
||||
.stream()
|
||||
.filter((it) -> it instanceof UsernamePasswordAuthenticationFilter)
|
||||
@@ -102,25 +102,25 @@ public class DefaultFiltersTests {
|
||||
List<SecurityFilterChain> filterChains = this.spring.getContext()
|
||||
.getBean(FilterChainProxy.class)
|
||||
.getFilterChains();
|
||||
assertThat(filterChains.size()).isEqualTo(2);
|
||||
assertThat(filterChains).hasSize(2);
|
||||
DefaultSecurityFilterChain firstFilter = (DefaultSecurityFilterChain) filterChains.get(0);
|
||||
DefaultSecurityFilterChain secondFilter = (DefaultSecurityFilterChain) filterChains.get(1);
|
||||
assertThat(firstFilter.getFilters().isEmpty()).isEqualTo(true);
|
||||
assertThat(secondFilter.getRequestMatcher()).isInstanceOf(AnyRequestMatcher.class);
|
||||
List<? extends Class<? extends Filter>> classes = secondFilter.getFilters()
|
||||
List<Class<? extends Filter>> classes = secondFilter.getFilters()
|
||||
.stream()
|
||||
.map(Filter::getClass)
|
||||
.collect(Collectors.toList());
|
||||
assertThat(classes.contains(WebAsyncManagerIntegrationFilter.class)).isTrue();
|
||||
assertThat(classes.contains(SecurityContextHolderFilter.class)).isTrue();
|
||||
assertThat(classes.contains(HeaderWriterFilter.class)).isTrue();
|
||||
assertThat(classes.contains(LogoutFilter.class)).isTrue();
|
||||
assertThat(classes.contains(CsrfFilter.class)).isTrue();
|
||||
assertThat(classes.contains(RequestCacheAwareFilter.class)).isTrue();
|
||||
assertThat(classes.contains(SecurityContextHolderAwareRequestFilter.class)).isTrue();
|
||||
assertThat(classes.contains(AnonymousAuthenticationFilter.class)).isTrue();
|
||||
assertThat(classes.contains(ExceptionTranslationFilter.class)).isTrue();
|
||||
assertThat(classes.contains(FilterSecurityInterceptor.class)).isTrue();
|
||||
assertThat(classes).contains(WebAsyncManagerIntegrationFilter.class);
|
||||
assertThat(classes).contains(SecurityContextHolderFilter.class);
|
||||
assertThat(classes).contains(HeaderWriterFilter.class);
|
||||
assertThat(classes).contains(LogoutFilter.class);
|
||||
assertThat(classes).contains(CsrfFilter.class);
|
||||
assertThat(classes).contains(RequestCacheAwareFilter.class);
|
||||
assertThat(classes).contains(SecurityContextHolderAwareRequestFilter.class);
|
||||
assertThat(classes).contains(AnonymousAuthenticationFilter.class);
|
||||
assertThat(classes).contains(ExceptionTranslationFilter.class);
|
||||
assertThat(classes).contains(FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -96,7 +96,7 @@ public class DefaultLoginPageConfigurerTests {
|
||||
+ " <meta name=\"author\" content=\"\">\n"
|
||||
+ " <title>Please sign in</title>\n"
|
||||
+ " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" integrity=\"sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " </head>\n"
|
||||
+ " <body>\n"
|
||||
+ " <div class=\"container\">\n"
|
||||
@@ -145,7 +145,7 @@ public class DefaultLoginPageConfigurerTests {
|
||||
+ " <meta name=\"author\" content=\"\">\n"
|
||||
+ " <title>Please sign in</title>\n"
|
||||
+ " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" integrity=\"sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " </head>\n"
|
||||
+ " <body>\n"
|
||||
+ " <div class=\"container\">\n"
|
||||
@@ -197,7 +197,7 @@ public class DefaultLoginPageConfigurerTests {
|
||||
+ " <meta name=\"author\" content=\"\">\n"
|
||||
+ " <title>Please sign in</title>\n"
|
||||
+ " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" integrity=\"sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " </head>\n"
|
||||
+ " <body>\n"
|
||||
+ " <div class=\"container\">\n"
|
||||
@@ -250,7 +250,7 @@ public class DefaultLoginPageConfigurerTests {
|
||||
+ " <meta name=\"author\" content=\"\">\n"
|
||||
+ " <title>Please sign in</title>\n"
|
||||
+ " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" integrity=\"sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56\" crossorigin=\"anonymous\"/>\n"
|
||||
+ " </head>\n"
|
||||
+ " <body>\n"
|
||||
+ " <div class=\"container\">\n"
|
||||
|
||||
+13
-1
@@ -31,6 +31,8 @@ import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
@@ -50,7 +52,7 @@ public class HeadersConfigurerEagerHeadersTests {
|
||||
|
||||
@Test
|
||||
public void requestWhenHeadersEagerlyConfiguredThenHeadersAreWritten() throws Exception {
|
||||
this.spring.register(HeadersAtTheBeginningOfRequestConfig.class).autowire();
|
||||
this.spring.register(HeadersAtTheBeginningOfRequestConfig.class, HomeController.class).autowire();
|
||||
this.mvc.perform(get("/").secure(true))
|
||||
.andExpect(header().string("X-Content-Type-Options", "nosniff"))
|
||||
.andExpect(header().string("X-Frame-Options", "DENY"))
|
||||
@@ -83,4 +85,14 @@ public class HeadersConfigurerEagerHeadersTests {
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
private static class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -107,6 +107,7 @@ public class HttpBasicConfigurerTests {
|
||||
|
||||
@Test
|
||||
public void httpBasicWhenUsingCustomAuthenticationEntryPointThenResponseIncludesBasicChallenge() throws Exception {
|
||||
CustomAuthenticationEntryPointConfig.ENTRY_POINT = mock(AuthenticationEntryPoint.class);
|
||||
this.spring.register(CustomAuthenticationEntryPointConfig.class).autowire();
|
||||
this.mvc.perform(get("/"));
|
||||
verify(CustomAuthenticationEntryPointConfig.ENTRY_POINT).commence(any(HttpServletRequest.class),
|
||||
@@ -124,7 +125,7 @@ public class HttpBasicConfigurerTests {
|
||||
// SEC-3019
|
||||
@Test
|
||||
public void httpBasicWhenRememberMeConfiguredThenSetsRememberMeCookie() throws Exception {
|
||||
this.spring.register(BasicUsesRememberMeConfig.class).autowire();
|
||||
this.spring.register(BasicUsesRememberMeConfig.class, Home.class).autowire();
|
||||
MockHttpServletRequestBuilder rememberMeRequest = get("/").with(httpBasic("user", "password"))
|
||||
.param("remember-me", "true");
|
||||
this.mvc.perform(rememberMeRequest).andExpect(cookie().exists("remember-me"));
|
||||
@@ -242,7 +243,7 @@ public class HttpBasicConfigurerTests {
|
||||
@EnableWebSecurity
|
||||
static class CustomAuthenticationEntryPointConfig {
|
||||
|
||||
static AuthenticationEntryPoint ENTRY_POINT = mock(AuthenticationEntryPoint.class);
|
||||
static AuthenticationEntryPoint ENTRY_POINT;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
+1
-1
@@ -382,7 +382,7 @@ public class RequestCacheConfigurerTests {
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.requestCache((cache) -> cache.disable());
|
||||
.requestCache(RequestCacheConfigurer::disable);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
+1
-3
@@ -556,9 +556,7 @@ public class SessionManagementConfigurerTests {
|
||||
.sessionManagement((sessionManagement) ->
|
||||
sessionManagement
|
||||
.requireExplicitAuthenticationStrategy(false)
|
||||
.sessionFixation((sessionFixation) ->
|
||||
sessionFixation.newSession()
|
||||
)
|
||||
.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession)
|
||||
)
|
||||
.httpBasic(withDefaults());
|
||||
// @formatter:on
|
||||
|
||||
+596
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.util.UrlUtils;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import okhttp3.mockwebserver.Dispatcher;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.Customizer;
|
||||
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.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimNames;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
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.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcLogoutConfigurer}
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class OidcLogoutConfigurerTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired(required = false)
|
||||
private MockWebServer web;
|
||||
|
||||
@Autowired
|
||||
private ClientRegistration clientRegistration;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Test
|
||||
void logoutWhenDefaultsThenRemotelyInvalidatesSessions() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession session = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isOk());
|
||||
this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenInvalidLogoutTokenThenBadRequest() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire();
|
||||
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MvcResult result = this.mvc.perform(get("/oauth2/authorization/" + registrationId))
|
||||
.andExpect(status().isFound())
|
||||
.andReturn();
|
||||
MockHttpSession session = (MockHttpSession) result.getRequest().getSession();
|
||||
String redirectUrl = UrlUtils.decode(result.getResponse().getRedirectedUrl());
|
||||
String state = this.mvc
|
||||
.perform(get(redirectUrl)
|
||||
.with(httpBasic(this.clientRegistration.getClientId(), this.clientRegistration.getClientSecret())))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
result = this.mvc
|
||||
.perform(get("/login/oauth2/code/" + registrationId).param("code", "code")
|
||||
.param("state", state)
|
||||
.session(session))
|
||||
.andExpect(status().isFound())
|
||||
.andReturn();
|
||||
session = (MockHttpSession) result.getRequest().getSession();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", "invalid"))
|
||||
.andExpect(status().isBadRequest());
|
||||
this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenLogoutTokenSpecifiesOneSessionThenRemotelyInvalidatesOnlyThatSession() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession one = login();
|
||||
MockHttpSession two = login();
|
||||
MockHttpSession three = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout").session(one))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isOk());
|
||||
this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isUnauthorized());
|
||||
this.mvc.perform(get("/token/logout").session(two)).andExpect(status().isOk());
|
||||
logoutToken = this.mvc.perform(get("/token/logout/all").session(three))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isOk());
|
||||
this.mvc.perform(get("/token/logout").session(two)).andExpect(status().isUnauthorized());
|
||||
this.mvc.perform(get("/token/logout").session(three)).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
||||
LogoutHandler logoutHandler = this.spring.getContext().getBean(LogoutHandler.class);
|
||||
willThrow(IllegalStateException.class).given(logoutHandler).logout(any(), any(), any());
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession one = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout/all").session(one))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(content().string(containsString("partial_logout")));
|
||||
this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenCustomComponentsThenUses() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithCustomComponentsConfig.class)
|
||||
.autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession session = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isOk());
|
||||
this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
|
||||
OidcSessionRegistry sessionRegistry = this.spring.getContext().getBean(OidcSessionRegistry.class);
|
||||
verify(sessionRegistry).saveSessionInformation(any());
|
||||
verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class));
|
||||
}
|
||||
|
||||
private MockHttpSession login() throws Exception {
|
||||
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
|
||||
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MvcResult result = this.mvc.perform(get("/oauth2/authorization/" + registrationId))
|
||||
.andExpect(status().isFound())
|
||||
.andReturn();
|
||||
MockHttpSession session = (MockHttpSession) result.getRequest().getSession();
|
||||
String redirectUrl = UrlUtils.decode(result.getResponse().getRedirectedUrl());
|
||||
String state = this.mvc
|
||||
.perform(get(redirectUrl)
|
||||
.with(httpBasic(this.clientRegistration.getClientId(), this.clientRegistration.getClientSecret())))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
result = this.mvc
|
||||
.perform(get("/login/oauth2/code/" + registrationId).param("code", "code")
|
||||
.param("state", state)
|
||||
.session(session))
|
||||
.andExpect(status().isFound())
|
||||
.andReturn();
|
||||
session = (MockHttpSession) result.getRequest().getSession();
|
||||
dispatcher.registerSession(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class RegistrationConfig {
|
||||
|
||||
@Autowired(required = false)
|
||||
MockWebServer web;
|
||||
|
||||
@Bean
|
||||
ClientRegistration clientRegistration() {
|
||||
if (this.web == null) {
|
||||
return TestClientRegistrations.clientRegistration().build();
|
||||
}
|
||||
String issuer = this.web.url("/").toString();
|
||||
return TestClientRegistrations.clientRegistration()
|
||||
.issuerUri(issuer)
|
||||
.jwkSetUri(issuer + "jwks")
|
||||
.tokenUri(issuer + "token")
|
||||
.userInfoUri(issuer + "user")
|
||||
.scope("openid")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
|
||||
return new InMemoryClientRegistrationRepository(clientRegistration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class DefaultConfig {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class WithCustomComponentsConfig {
|
||||
|
||||
OidcSessionRegistry sessionRegistry = spy(new InMemoryOidcSessionRegistry());
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
|
||||
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OidcSessionRegistry sessionRegistry() {
|
||||
return this.sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class WithBrokenLogoutConfig {
|
||||
|
||||
private final LogoutHandler logoutHandler = mock(LogoutHandler.class);
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.logout((logout) -> logout.addLogoutHandler(this.logoutHandler))
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
LogoutHandler logoutHandler() {
|
||||
return this.logoutHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
@RestController
|
||||
static class OidcProviderConfig {
|
||||
|
||||
private static final RSAKey key = key();
|
||||
|
||||
private static final JWKSource<SecurityContext> jwks = jwks(key);
|
||||
|
||||
private static RSAKey key() {
|
||||
try {
|
||||
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||
return new RSAKey.Builder((RSAPublicKey) pair.getPublic()).privateKey(pair.getPrivate()).build();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static JWKSource<SecurityContext> jwks(RSAKey key) {
|
||||
try {
|
||||
return new ImmutableJWKSet<>(new JWKSet(key));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final String username = "user";
|
||||
|
||||
private final JwtEncoder encoder = new NimbusJwtEncoder(jwks);
|
||||
|
||||
private String nonce;
|
||||
|
||||
@Autowired
|
||||
ClientRegistration registration;
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.securityMatcher("/jwks", "/login/oauth/authorize", "/nonce", "/token", "/token/logout", "/user")
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/jwks").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.oauth2ResourceServer((oauth2) -> oauth2
|
||||
.jwt((jwt) -> jwt.jwkSetUri(registration.getProviderDetails().getJwkSetUri()))
|
||||
);
|
||||
// @formatter:off
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService users(ClientRegistration registration) {
|
||||
return new InMemoryUserDetailsManager(User.withUsername(registration.getClientId())
|
||||
.password("{noop}" + registration.getClientSecret()).authorities("APP").build());
|
||||
}
|
||||
|
||||
@GetMapping("/login/oauth/authorize")
|
||||
String nonce(@RequestParam("nonce") String nonce, @RequestParam("state") String state) {
|
||||
this.nonce = nonce;
|
||||
return state;
|
||||
}
|
||||
|
||||
@PostMapping("/token")
|
||||
Map<String, Object> accessToken(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||
.from(JwtClaimsSet.builder().id("id").subject(this.username)
|
||||
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
|
||||
String token = this.encoder.encode(parameters).getTokenValue();
|
||||
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
|
||||
.toJSONObject();
|
||||
}
|
||||
|
||||
String idToken(String sessionId) {
|
||||
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
|
||||
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
|
||||
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
|
||||
.claim(LogoutTokenClaimNames.SID, sessionId).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||
.from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build());
|
||||
return this.encoder.encode(parameters).getTokenValue();
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
Map<String, Object> userinfo() {
|
||||
return Map.of("sub", this.username, "id", this.username);
|
||||
}
|
||||
|
||||
@GetMapping("/jwks")
|
||||
String jwks() {
|
||||
return new JWKSet(key).toString();
|
||||
}
|
||||
|
||||
@GetMapping("/token/logout")
|
||||
String logoutToken(@AuthenticationPrincipal OidcUser user) {
|
||||
OidcLogoutToken token = TestOidcLogoutTokens.withUser(user)
|
||||
.audience(List.of(this.registration.getClientId())).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||
.from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build());
|
||||
return this.encoder.encode(parameters).getTokenValue();
|
||||
}
|
||||
|
||||
@GetMapping("/token/logout/all")
|
||||
String logoutTokenAll(@AuthenticationPrincipal OidcUser user) {
|
||||
OidcLogoutToken token = TestOidcLogoutTokens.withUser(user)
|
||||
.audience(List.of(this.registration.getClientId()))
|
||||
.claims((claims) -> claims.remove(LogoutTokenClaimNames.SID)).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||
.from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build());
|
||||
return this.encoder.encode(parameters).getTokenValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class WebServerConfig {
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
@Bean
|
||||
MockWebServer web(ObjectProvider<MockMvc> mvc) {
|
||||
this.server.setDispatcher(new MockMvcDispatcher(mvc));
|
||||
return this.server;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockMvcDispatcher extends Dispatcher {
|
||||
|
||||
private final Map<String, MockHttpSession> session = new ConcurrentHashMap<>();
|
||||
|
||||
private final ObjectProvider<MockMvc> mvcProvider;
|
||||
|
||||
private MockMvc mvc;
|
||||
|
||||
MockMvcDispatcher(ObjectProvider<MockMvc> mvc) {
|
||||
this.mvcProvider = mvc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
||||
this.mvc = this.mvcProvider.getObject();
|
||||
String method = request.getMethod();
|
||||
String path = request.getPath();
|
||||
String csrf = request.getHeader("X-CSRF-TOKEN");
|
||||
MockHttpSession session = session(request);
|
||||
MockHttpServletRequestBuilder builder;
|
||||
if ("GET".equals(method)) {
|
||||
builder = get(path);
|
||||
}
|
||||
else {
|
||||
builder = post(path).content(request.getBody().readUtf8());
|
||||
if (csrf != null) {
|
||||
builder.header("X-CSRF-TOKEN", csrf);
|
||||
}
|
||||
else {
|
||||
builder.with(csrf());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, List<String>> header : request.getHeaders().toMultimap().entrySet()) {
|
||||
builder.header(header.getKey(), header.getValue().iterator().next());
|
||||
}
|
||||
try {
|
||||
MockHttpServletResponse mvcResponse = this.mvc.perform(builder.session(session)).andReturn().getResponse();
|
||||
return toMockResponse(mvcResponse);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
MockResponse response = new MockResponse();
|
||||
response.setResponseCode(500);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
void registerSession(MockHttpSession session) {
|
||||
this.session.put(session.getId(), session);
|
||||
}
|
||||
|
||||
private MockHttpSession session(RecordedRequest request) {
|
||||
String cookieHeaderValue = request.getHeader("Cookie");
|
||||
if (cookieHeaderValue == null) {
|
||||
return new MockHttpSession();
|
||||
}
|
||||
String[] cookies = cookieHeaderValue.split(";");
|
||||
for (String cookie : cookies) {
|
||||
String[] parts = cookie.split("=");
|
||||
if ("JSESSIONID".equals(parts[0])) {
|
||||
return this.session.computeIfAbsent(parts[1],
|
||||
(k) -> new MockHttpSession(new MockServletContext(), parts[1]));
|
||||
}
|
||||
}
|
||||
return new MockHttpSession();
|
||||
}
|
||||
|
||||
private MockResponse toMockResponse(MockHttpServletResponse mvcResponse) {
|
||||
MockResponse response = new MockResponse();
|
||||
response.setResponseCode(mvcResponse.getStatus());
|
||||
for (String name : mvcResponse.getHeaderNames()) {
|
||||
response.addHeader(name, mvcResponse.getHeaderValue(name));
|
||||
}
|
||||
response.setBody(getContentAsString(mvcResponse));
|
||||
return response;
|
||||
}
|
||||
|
||||
private String getContentAsString(MockHttpServletResponse response) {
|
||||
try {
|
||||
return response.getContentAsString();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+2
-5
@@ -873,8 +873,7 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
context.registerBean("decoderTwo", JwtDecoder.class, () -> decoder);
|
||||
this.spring.context(context).autowire();
|
||||
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> jwtConfigurer.getJwtDecoder());
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(jwtConfigurer::getJwtDecoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1897,9 +1896,7 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Login(withDefaults())
|
||||
.oauth2ResourceServer((oauth2) -> oauth2
|
||||
.jwt()
|
||||
);
|
||||
.oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
+38
-6
@@ -18,17 +18,25 @@ package org.springframework.security.config.annotation.web.configurers.saml2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.core.xml.io.Marshaller;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -55,6 +63,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextChangedListener;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2Utils;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
@@ -64,6 +73,7 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
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;
|
||||
@@ -112,7 +122,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class Saml2LoginConfigurerTests {
|
||||
|
||||
private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=";
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
|
||||
private static final RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials()
|
||||
.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()))
|
||||
.assertingPartyDetails((party) -> party
|
||||
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
|
||||
.build();
|
||||
|
||||
private static String SIGNED_RESPONSE;
|
||||
|
||||
private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class);
|
||||
|
||||
@@ -139,6 +159,23 @@ public class Saml2LoginConfigurerTests {
|
||||
|
||||
private MockFilterChain filterChain;
|
||||
|
||||
@BeforeAll
|
||||
static void createResponse() throws Exception {
|
||||
String destination = registration.getAssertionConsumerServiceLocation();
|
||||
String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId();
|
||||
String relyingPartyEntityId = registration.getEntityId();
|
||||
Response response = TestOpenSamlObjects.response(destination, assertingPartyEntityId);
|
||||
Assertion assertion = TestOpenSamlObjects.assertion("test@saml.user", assertingPartyEntityId,
|
||||
relyingPartyEntityId, destination);
|
||||
response.getAssertions().add(assertion);
|
||||
Response signed = TestOpenSamlObjects.signed(response,
|
||||
registration.getSigningX509Credentials().iterator().next(), relyingPartyEntityId);
|
||||
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(signed);
|
||||
Element element = marshaller.marshall(signed);
|
||||
String serialized = SerializeSupport.nodeToString(element);
|
||||
SIGNED_RESPONSE = Saml2Utils.samlEncode(serialized.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.request = new MockHttpServletRequest("POST", "");
|
||||
@@ -709,11 +746,6 @@ public class Saml2LoginConfigurerTests {
|
||||
|
||||
@Bean
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials()
|
||||
.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartySigningCredential()))
|
||||
.assertingPartyDetails((party) -> party.verificationX509Credentials(
|
||||
(c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
|
||||
.build();
|
||||
return spy(new InMemoryRelyingPartyRegistrationRepository(registration));
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -286,8 +286,8 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
private void assertHandshake(HttpServletRequest request) {
|
||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
|
||||
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
|
||||
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
|
||||
assertThat(handshakeHandler.attributes).containsEntry(this.sessionAttr,
|
||||
request.getSession().getAttribute(this.sessionAttr));
|
||||
}
|
||||
|
||||
private HttpRequestHandler handler(HttpServletRequest request) throws Exception {
|
||||
|
||||
+1
-2
@@ -27,8 +27,7 @@ public class SyncExecutorSubscribableChannelPostProcessor implements BeanPostPro
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof ExecutorSubscribableChannel) {
|
||||
ExecutorSubscribableChannel original = (ExecutorSubscribableChannel) bean;
|
||||
if (bean instanceof ExecutorSubscribableChannel original) {
|
||||
ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel();
|
||||
channel.setInterceptors(original.getInterceptors());
|
||||
return channel;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user