1
0
mirror of synced 2026-07-09 21:00:02 +00:00

Compare commits

...

105 Commits

Author SHA1 Message Date
Marcus Da Coregio fa0e06ebdc Release 5.6.3 2022-04-18 11:59:42 -03:00
Marcus Da Coregio 6724627b50 Update spring-ldap-core to 2.3.7.RELEASE
Closes gh-11127
2022-04-18 11:45:27 -03:00
Marcus Da Coregio 4aab7b06a9 Update org.springframework.data to 2021.1.3
Closes gh-11126
2022-04-18 11:45:24 -03:00
Marcus Da Coregio d0d4d58c48 Update org.springframework to 5.3.19
Closes gh-11125
2022-04-18 11:45:22 -03:00
Marcus Da Coregio 29be60d035 Update hibernate-entitymanager to 5.6.8.Final
Closes gh-11124
2022-04-18 11:45:19 -03:00
Marcus Da Coregio 5024103b07 Update org.eclipse.jetty to 9.4.46.v20220331
Closes gh-11123
2022-04-18 11:45:17 -03:00
Marcus Da Coregio d0327807a7 Update org.aspectj to 1.9.9.1
Closes gh-11122
2022-04-18 11:45:14 -03:00
Marcus Da Coregio f880f1ba26 Update io.rsocket to 1.1.2
Closes gh-11121
2022-04-18 11:45:12 -03:00
Marcus Da Coregio 71dafc0c06 Update io.projectreactor to 2020.0.18
Closes gh-11119
2022-04-18 11:45:07 -03:00
Marcus Da Coregio 7d9357b871 Update mockk to 1.12.3
Closes gh-11118
2022-04-18 11:45:05 -03:00
Marcus Da Coregio 0b02bbecd1 Update jackson-bom to 2.13.2.20220328
Closes gh-11115
2022-04-18 11:44:57 -03:00
Marcus Da Coregio 932a5cc8b4 Update logback-classic to 1.2.11
Closes gh-11114
2022-04-18 11:44:55 -03:00
Simone Giannino e4a321511a Update saganCreateRelease property referenceDocUrl
- Updated saganCreateRelease task with the new referenceDocUrl for reference documentation

Closes gh-11016
2022-04-01 13:08:18 -03:00
Johannes Graf 4ee5800ec9 use okta as registration id
looks like `ping` is some registration id used in the past.

Closes gh-11034
2022-03-30 14:41:03 -06:00
Marcus Da Coregio c67632225d Use ServletContext in AuthorizationManagerWebInvocationPrivilegeEvaluator
Closes gh-10908
2022-03-28 10:13:40 -03:00
Rob Winch 6e45a376cd Remove "Hi ... there" From Docs
Close gh-10963
2022-03-11 13:42:38 -06:00
Marcus Da Coregio 70b67cd2f1 AuthorizationManagerWebInvocationPrivilegeEvaluator grant access when AuthorizationManager abstains
Closes gh-10950
2022-03-09 15:22:21 -03:00
Josh Cummings 304e89041c Polish Formatting
Issue gh-10799
2022-03-02 16:40:13 -07:00
Sander van Schouwenburg f1a76efc2d Preserve order of RelyingPartRegistration credentials
Issue gh-10799
2022-03-02 16:40:13 -07:00
Josh Cummings 963251314b Replace Apache Commons Base64 Decoding
Issue gh-10923
2022-03-02 16:40:11 -07:00
Josh Cummings ee061f3659 Use RFC2045 Encoding for SAML 2.0 Logout
Closes gh-10923
2022-03-02 16:39:31 -07:00
Talerngpong Virojwutikul 440ffce2eb Update PasswordEncoder declaration
Closes gh-10910
2022-03-01 07:50:55 -07:00
Filip Hanik 47871562ca Change HashSet to LinkedHashSet
For various RelyingPartyRegistration.credentials to preserve order of insertion.

Issue gh-10799
2022-02-28 15:02:03 -07:00
Josh Cummings f0c548cee7 Invert Log Messages
Closes gh-10909
2022-02-28 13:17:01 -07:00
Marcus Da Coregio 1a32b399e6 Next development version 2022-02-21 10:49:06 -03:00
Marcus Da Coregio e9f3da94fc Release 5.6.2 2022-02-21 10:04:41 -03:00
Marcus Da Coregio 4fb6f936a7 Update spring-ldap-core to 2.3.6.RELEASE
Closes gh-10877
2022-02-21 09:20:57 -03:00
Marcus Da Coregio c431b5479e Update org.springframework.data to 2021.1.2
Closes gh-10876
2022-02-21 09:20:54 -03:00
Marcus Da Coregio fc03950afd Update org.springframework to 5.3.16
Closes gh-10875
2022-02-21 09:20:52 -03:00
Marcus Da Coregio 516b5fed4a Update org.slf4j to 1.7.36
Closes gh-10874
2022-02-21 09:20:49 -03:00
Marcus Da Coregio 165e29e585 Update hibernate-entitymanager to 5.6.5.Final
Closes gh-10873
2022-02-21 09:20:47 -03:00
Marcus Da Coregio 0be3e86b21 Update org.eclipse.jetty to 9.4.45.v20220203
Closes gh-10872
2022-02-21 09:20:44 -03:00
Marcus Da Coregio 96d229a6d1 Update org.aspectj to 1.9.8
Closes gh-10871
2022-02-21 09:20:42 -03:00
Marcus Da Coregio 5098cad9cf Update io.spring.javaformat to 0.0.31
Closes gh-10870
2022-02-21 09:20:40 -03:00
Marcus Da Coregio 883c480af0 Update r2dbc-h2 to 0.8.5.RELEASE
Closes gh-10869
2022-02-21 09:20:37 -03:00
Marcus Da Coregio 0f83e40427 Update io.projectreactor to 2020.0.16
Closes gh-10867
2022-02-21 09:20:32 -03:00
Marcus Da Coregio ad6ef6e4c7 Update mockk to 1.12.2
Closes gh-10866
2022-02-21 09:20:30 -03:00
Marcus Da Coregio 94201e46f1 Update logback-classic to 1.2.10
Closes gh-10865
2022-02-21 09:20:27 -03:00
Rob Winch 910e198cee Add .sdkmanrc 2022-02-18 13:48:26 -06:00
Talerngpong Virojwutikul 45a88fc391 add Kotlin examples for Spring Data Integration of servlet application 2022-02-18 08:49:27 -03:00
giger85 8f01efb9e3 Replace StringUtils class completely
Issue gh-9925
Closes gh-10805
2022-02-10 17:50:07 +01:00
Josh Cummings f53c65b3a0 Polish ignoring() log messaging
- Public API remains unchanged

Issue gh-9334
2022-02-07 15:07:29 -07:00
Manuel Jordan 0be772ff5b Print ignore message DefaultSecurityFilterChain
When either `web.ignoring().mvcMatchers(...)` or
`web.ignoring().antMatchers(...)` methods are used, for all their
variations, the DefaultSecurityFilterChain class now indicates
correctly through its ouput what paths are ignored according the
`ignoring()` settings.

Closes gh-9334
2022-02-07 15:07:29 -07:00
Rob Winch ac990afa5d Document Authorize HTTP Requests for Reactive Security
Closes gh-10801
2022-02-03 13:47:35 -06:00
Ken Dombeck 55cccbf727 Fix broken link to SAML2 login example 2022-02-03 10:20:45 -06:00
Eleftheria Stein a095ea75a2 Fix typo in getting started docs
Closes gh-10736
2022-02-02 10:52:54 +01:00
Marcus Da Coregio a041e7c943 RequestMatcherDelegatingWebInvocationPrivilegeEvaluator doesn't provided access to the ServletContext
Closes gh-10779
2022-01-31 09:50:17 -03:00
Rob Winch 4f3072b3d9 Exclude javax from hibernate dependency
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 13c467734a Remove javax.transaction
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch c01b2b946b Additional removal of javax.inject
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 549f9dab64 dependencies jakarta.transaction-api
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 58090c37ea jsr250-api -> jakarta.annotation-api
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 38344e8de6 javax.servlet.jsp-api -> jakarta.servlet.jsp-api
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 6f319f0b13 javax.servlet.jsp.jstl-api -> jakarta.servlet.jsp.jstl-api
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 4e71b26ea4 Exclude javax from cas-client-core
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 04f3bbcefa javax.xml.bind:jaxb-api -> jakarta.xml.bind:jakarta.xml.bind-api
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch c67ee6f2a8 javax.servlet:javax.servlet-api -> jakarta.servlet:jakarta.servlet-api
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 4b590ce31f Remove javax.inject
Issue gh-10501
2022-01-19 15:32:12 -06:00
Rob Winch 5902b46e9b Remove jcl-over-slf4j
Issue gh-10499

# Conflicts:
#	dependencies/spring-security-dependencies.gradle
2022-01-19 15:32:01 -06:00
Rob Winch 62449d6fa2 Remove commons-logging
Closes gh-10499
2022-01-19 15:31:22 -06:00
Rob Winch aa06088077 Add CheckProhibitedDependenciesLifecyclePlugin
Issue gh-10501
2022-01-19 15:30:34 -06:00
Rob Winch 2a60776c8a Add CheckClasspathForProhibitedDependencies
Issues gh-10499 gh-10501
2022-01-19 15:30:34 -06:00
Rob Winch f411ecada2 CheckAntoraVersionTask has optional properties 2022-01-18 14:29:39 -06:00
Rob Winch 8eeacf4a36 Fix Antora for Milestone & RC
- Verify Antora display_version
- Run workflow for tags
- Allow run workflow manually

Issue gh-10765
2022-01-18 14:20:47 -06:00
Josh Cummings 20c252982e Remove SAML 2.0 Logout Default
Closes gh-10607
2022-01-14 15:29:50 -07:00
Josh Cummings 6c5ac0d8ec Use noNullElements
Collection#contains(null) does not work for all collection types

Closes gh-10703
2022-01-14 15:09:21 -07:00
Marcus Da Coregio e1cb375fbf Make source code compatible with JDK 8
Closes gh-10695
2022-01-12 16:39:50 -03:00
Marcus Da Coregio e7e3f06044 Fix @since tag
Issue gh-10590, gh-10554
2022-01-06 13:22:13 -03:00
Marcus Da Coregio 994e93741b Configure WebInvocationPrivilegeEvaluator bean for multiple filter chains
Closes gh-10554
2022-01-05 14:06:47 -03:00
Marcus Da Coregio 04e1a11e35 Add RequestMatcherEntry 2022-01-05 14:06:47 -03:00
Marcus Da Coregio 547056d5cc Introduce AuthorizationManagerWebInvocationPrivilegeEvaluator
Closes gh-10590
2022-01-05 14:06:47 -03:00
Rob Winch 1fa35fb9a8 Antora prerelease: true for milestone and rc 2022-01-05 10:00:56 -06:00
Rob Winch a49914e631 Update RELEASE.adoc for antora.yml 2022-01-04 15:52:05 -06:00
Rob Winch 783a43ea35 Add CheckAntoraVersionPlugin 2022-01-04 15:52:05 -06:00
Rob Winch 07a176d11e Update antora to 5.6.2 2022-01-04 15:52:05 -06:00
Josh Cummings c664fbc1a3 Support No SingleLogoutServiceLocation
Closes gh-10674
2022-01-03 13:38:47 -07:00
Steve Riesenberg af412c4d65 Next Development Version 2021-12-20 15:24:46 -06:00
Steve Riesenberg e38bf6e554 Release 5.6.1 2021-12-20 14:14:55 -06:00
Steve Riesenberg 624e0da669 Update spring-ldap-core to 2.3.5.RELEASE
Closes gh-10659
2021-12-20 12:28:54 -06:00
Steve Riesenberg b28aa6c647 Update org.springframework to 5.3.14
Closes gh-10658
2021-12-20 12:28:52 -06:00
Steve Riesenberg 9e83b4be25 Update junit-bom to 5.8.2
Closes gh-10656
2021-12-20 12:28:47 -06:00
Steve Riesenberg e9854c91ef Update org.jetbrains.kotlin to 1.5.32
Closes gh-10655
2021-12-20 12:28:45 -06:00
Steve Riesenberg 0345e29afb Update cas-client-core to 3.6.4
Closes gh-10654
2021-12-20 12:28:43 -06:00
Steve Riesenberg 8bd5795f8e Update hibernate-entitymanager to 5.6.3.Final
Closes gh-10653
2021-12-20 12:28:41 -06:00
Steve Riesenberg 4fbc98dcd2 Update io.projectreactor to 2020.0.14
Closes gh-10651
2021-12-20 12:28:36 -06:00
Steve Riesenberg 44cdbd6cf3 Update mockk to 1.12.1
Closes gh-10650
2021-12-20 12:28:34 -06:00
Steve Riesenberg a9af8c4128 Update jackson-bom to 2.13.1
Closes gh-10647
2021-12-20 12:28:28 -06:00
Steve Riesenberg 28c81af788 Update logback-classic to 1.2.9
Closes gh-10646
2021-12-20 12:28:26 -06:00
Steve Riesenberg 3a956daf0c Exclude minor version bump 2021-12-20 12:16:04 -06:00
Marcus Da Coregio 9c245865d7 Point to samples on 5.6.x branch 2021-12-10 14:38:24 -03:00
Guirong Hu 86ed937a47 Fix the bug that the custom GrantedAuthority comparison fails
Closes gh-10566
2021-12-08 08:51:54 -03:00
Steve Riesenberg 32ec8c3ae4 Fix Reactive OAuth2 Kotlin DSL examples
Closes gh-10580
2021-12-07 13:58:24 +01:00
Steve Riesenberg c7ffd2513a Update copyright year
Issue gh-10557
2021-12-01 17:36:19 -06:00
Steve Riesenberg b3e0f167ff Fix case sensitive headers comparison
Closes gh-10557
2021-12-01 15:01:06 -06:00
Jonas Erbe 8c063f8ccb Fix JwtClaimValidator wrong error code
Previously JwtClaimValidator returned the invalid_request error on claim validation failure.
But validators have to return invalid_token errors on failure according to:
https://datatracker.ietf.org/doc/html/rfc6750#section-3.1.
Also see gh-10337

Closes gh-10337
2021-11-29 12:13:24 -07:00
Henning Poettker f1ca42e501 Fix return type for NoOpPasswordEncoder bean in documentation 2021-11-23 14:14:05 -03:00
Lars Grefer 719149f01d Remove usages of Gradle's jcenter() repository
Closes gh-10253
2021-11-22 09:38:49 -03:00
Lars Grefer ccd30607a9 Fix Gradle Deprecation Warnings 2021-11-22 09:38:44 -03:00
Dávid Kováč aa1ef46d84 Update clockSkew javadoc according to implementation
Closes gh-10174
2021-11-19 13:33:05 +01:00
Josh Cummings 16a21264d0 Polish AuthRequestConverter Sample Doc
Issue gh-10364
2021-11-18 13:36:31 -07:00
Norbert Nowak 1c0f092176 Fix AuthnRequestConverter Sample Typos
Closes gh-10364
2021-11-18 13:26:53 -07:00
Jeff Maxwell 32d79f3f4e Fix setJWTClaimSetJWSKeySelector Typo
Closes gh-10504
2021-11-16 15:33:42 -07:00
Jeff Maxwell 088a24cf59 Fix jwtDecoder Documentation Usage
Closes gh-10505
2021-11-16 15:18:42 -07:00
Josh Cummings 6b6f473a1b Added authorizeHttpRequests Docs
Closes gh-10442
2021-11-15 16:23:18 -07:00
Rob Winch 29a4b2bc9b Next Development Version 2021-11-15 16:13:45 -06:00
123 changed files with 2738 additions and 440 deletions
+2
View File
@@ -1,9 +1,11 @@
name: Generate Antora Files and Request Build
on:
workflow_dispatch:
push:
branches-ignore:
- 'gh-pages'
tags: '**'
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
+6
View File
@@ -0,0 +1,6 @@
# Use sdkman to run "sdk env" to initialize with correct JDK version
# Enable auto-env through the sdkman_auto_env config
# See https://sdkman.io/usage#config
# A summary is to add the following to ~/.sdkman/etc/config
# sdkman_auto_env=true
java=11.0.14-tem
+11 -1
View File
@@ -78,6 +78,16 @@ Alternatively, you can manually check using https://github.com/spring-projects/s
Update the version number in `gradle.properties` for the release, for example `5.5.0-M1`, `5.5.0-RC1`, `5.5.0`
= Update Antora Version
You will need to update the antora.yml version.
If you are unsure of what the values should be, the following task will instruct you what the expected values are:
[source,bash]
----
./gradlew :spring-security-docs:antoraCheckVersion
----
= Build Locally
Run the build using
@@ -119,7 +129,7 @@ git push origin 5.4.0-RC1
== 7. Update to Next Development Version
* Update `gradle.properties` version to next `+SNAPSHOT+` version and then push
* Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml, and then push
== 8. Update version on project page
+3 -1
View File
@@ -35,7 +35,7 @@ repositories {
}
tasks.named("saganCreateRelease") {
referenceDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/"
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/"
}
@@ -63,6 +63,7 @@ updateDependenciesSettings {
})
dependencyExcludes {
majorVersionBump()
minorVersionBump()
alphaBetaVersions()
releaseCandidatesVersions()
milestoneVersions()
@@ -105,6 +106,7 @@ subprojects {
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
options.release = 8
}
}
+12 -4
View File
@@ -9,7 +9,6 @@ plugins {
sourceCompatibility = 1.8
repositories {
jcenter()
gradlePluginPortal()
mavenCentral()
maven { url 'https://repo.spring.io/plugins-release/' }
@@ -28,6 +27,10 @@ sourceSets {
gradlePlugin {
plugins {
checkAntoraVersion {
id = "org.springframework.antora.check-version"
implementationClass = "org.springframework.gradle.antora.CheckAntoraVersionPlugin"
}
trang {
id = "trang"
implementationClass = "trang.TrangPlugin"
@@ -73,10 +76,11 @@ dependencies {
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.thaiopensource:trang:20091111'
implementation 'net.sourceforge.saxon:saxon:9.1.0.8'
implementation 'org.yaml:snakeyaml:1.30'
implementation localGroovy()
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
implementation 'io.projectreactor:reactor-core:3.4.12'
implementation 'io.projectreactor:reactor-core:3.4.17'
implementation 'gradle.plugin.org.gretty:gretty:3.0.1'
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
@@ -88,7 +92,7 @@ dependencies {
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20'
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
testImplementation platform('org.junit:junit-bom:5.8.1')
testImplementation platform('org.junit:junit-bom:5.8.2')
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
testImplementation "org.junit.jupiter:junit-jupiter-engine"
@@ -100,7 +104,11 @@ dependencies {
}
test {
tasks.named('test', Test).configure {
onlyIf { !project.hasProperty("buildSrc.skipTests") }
useJUnitPlatform()
jvmArgs(
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED'
)
}
@@ -25,7 +25,7 @@ public class DocsPlugin implements Plugin<Project> {
group = 'Distribution'
archiveBaseName = project.rootProject.name
archiveClassifier = 'docs'
description = "Builds -${classifier} archive containing all " +
description = "Builds -${archiveClassifier.get()} archive containing all " +
"Docs for deployment at docs.spring.io"
from(project.tasks.api.outputs) {
@@ -35,11 +35,6 @@ class RepositoryConventionPlugin implements Plugin<Project> {
mavenLocal()
}
mavenCentral()
jcenter() {
content {
includeGroup "org.gretty"
}
}
if (isSnapshot) {
maven {
name = 'artifactory-snapshot'
@@ -21,6 +21,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.PluginManager
import org.springframework.gradle.classpath.CheckProhibitedDependenciesLifecyclePlugin
import org.springframework.gradle.maven.SpringNexusPublishPlugin
class RootProjectPlugin implements Plugin<Project> {
@@ -32,6 +33,7 @@ class RootProjectPlugin implements Plugin<Project> {
pluginManager.apply(SchemaPlugin)
pluginManager.apply(NoHttpPlugin)
pluginManager.apply(SpringNexusPublishPlugin)
pluginManager.apply(CheckProhibitedDependenciesLifecyclePlugin)
pluginManager.apply("org.sonarqube")
project.repositories.mavenCentral()
@@ -20,6 +20,7 @@ import org.gradle.api.Project
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.MavenPlugin;
import org.gradle.api.plugins.PluginManager
import org.springframework.gradle.classpath.CheckClasspathForProhibitedDependenciesPlugin;
import org.springframework.gradle.maven.SpringMavenPlugin;
/**
@@ -32,6 +33,7 @@ class SpringModulePlugin extends AbstractSpringJavaPlugin {
PluginManager pluginManager = project.getPluginManager();
pluginManager.apply(JavaLibraryPlugin.class)
pluginManager.apply(SpringMavenPlugin.class);
pluginManager.apply(CheckClasspathForProhibitedDependenciesPlugin.class);
pluginManager.apply("io.spring.convention.jacoco");
def deployArtifacts = project.task("deployArtifacts")
@@ -0,0 +1,85 @@
package org.springframework.gradle.antora;
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;
public class CheckAntoraVersionPlugin implements Plugin<Project> {
public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion";
@Override
public void apply(Project project) {
TaskProvider<CheckAntoraVersionTask> antoraCheckVersion = project.getTasks().register(ANTORA_CHECK_VERSION_TASK_NAME, CheckAntoraVersionTask.class, new Action<CheckAntoraVersionTask>() {
@Override
public void execute(CheckAntoraVersionTask antoraCheckVersion) {
antoraCheckVersion.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
antoraCheckVersion.setDescription("Checks the antora.yml version properties match the Gradle version");
antoraCheckVersion.getAntoraVersion().convention(project.provider(() -> getDefaultAntoraVersion(project)));
antoraCheckVersion.getAntoraPrerelease().convention(project.provider(() -> getDefaultAntoraPrerelease(project)));
antoraCheckVersion.getAntoraDisplayVersion().convention(project.provider(() -> getDefaultAntoraDisplayVersion(project)));
antoraCheckVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml")));
}
});
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);
}
});
}
});
}
private static String getDefaultAntoraVersion(Project project) {
String projectVersion = getProjectVersion(project);
int preReleaseIndex = getSnapshotIndex(projectVersion);
return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion;
}
private static String getDefaultAntoraPrerelease(Project project) {
String projectVersion = getProjectVersion(project);
if (isSnapshot(projectVersion)) {
int preReleaseIndex = getSnapshotIndex(projectVersion);
return projectVersion.substring(preReleaseIndex);
}
if (isPreRelease(projectVersion)) {
return Boolean.TRUE.toString();
}
return null;
}
private static String getDefaultAntoraDisplayVersion(Project project) {
String projectVersion = getProjectVersion(project);
if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) {
return getDefaultAntoraVersion(project);
}
return null;
}
private static String getProjectVersion(Project project) {
Object projectVersion = project.getVersion();
if (projectVersion == null) {
throw new GradleException("Please define antoraVersion and antoraPrerelease on " + ANTORA_CHECK_VERSION_TASK_NAME + " or provide a Project version so they can be defaulted");
}
return String.valueOf(projectVersion);
}
private static boolean isSnapshot(String projectVersion) {
return getSnapshotIndex(projectVersion) >= 0;
}
private static int getSnapshotIndex(String projectVersion) {
return projectVersion.lastIndexOf("-SNAPSHOT");
}
private static boolean isPreRelease(String projectVersion) {
return projectVersion.lastIndexOf("-") >= 0;
}
}
@@ -0,0 +1,96 @@
package org.springframework.gradle.antora;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public abstract class CheckAntoraVersionTask extends DefaultTask {
@TaskAction
public void check() throws FileNotFoundException {
File antoraYmlFile = getAntoraYmlFile().getAsFile().get();
String expectedAntoraVersion = getAntoraVersion().get();
String expectedAntoraPrerelease = getAntoraPrerelease().getOrElse(null);
String expectedAntoraDisplayVersion = getAntoraDisplayVersion().getOrElse(null);
Representer representer = new Representer();
representer.getPropertyUtils().setSkipMissingProperties(true);
Yaml yaml = new Yaml(new Constructor(AntoraYml.class), representer);
AntoraYml antoraYml = yaml.load(new FileInputStream(antoraYmlFile));
String actualAntoraPrerelease = antoraYml.getPrerelease();
boolean preReleaseMatches = antoraYml.getPrerelease() == null && expectedAntoraPrerelease == null ||
(actualAntoraPrerelease != null && actualAntoraPrerelease.equals(expectedAntoraPrerelease));
String actualAntoraDisplayVersion = antoraYml.getDisplay_version();
boolean displayVersionMatches = antoraYml.getDisplay_version() == null && expectedAntoraDisplayVersion == null ||
(actualAntoraDisplayVersion != null && actualAntoraDisplayVersion.equals(expectedAntoraDisplayVersion));
String actualAntoraVersion = antoraYml.getVersion();
if (!preReleaseMatches ||
!displayVersionMatches ||
!expectedAntoraVersion.equals(actualAntoraVersion)) {
throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '"
+ expectedAntoraVersion + "' prerelease: '" + expectedAntoraPrerelease + "' display_version: '"
+ expectedAntoraDisplayVersion + "' defined in " + antoraYmlFile + " but got version: '"
+ actualAntoraVersion + "' prerelease: '" + actualAntoraPrerelease + "' display_version: '" + actualAntoraDisplayVersion + "'");
}
}
@InputFile
public abstract RegularFileProperty getAntoraYmlFile();
@Input
public abstract Property<String> getAntoraVersion();
@Input
@Optional
public abstract Property<String> getAntoraPrerelease();
@Input
@Optional
public abstract Property<String> getAntoraDisplayVersion();
public static class AntoraYml {
private String version;
private String prerelease;
private String display_version;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getPrerelease() {
return prerelease;
}
public void setPrerelease(String prerelease) {
this.prerelease = prerelease;
}
public String getDisplay_version() {
return display_version;
}
public void setDisplay_version(String display_version) {
this.display_version = display_version;
}
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.classpath;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedConfiguration;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.TaskAction;
import java.io.IOException;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
* A {@link Task} for checking the classpath for prohibited dependencies.
*
* @author Andy Wilkinson
*/
public class CheckClasspathForProhibitedDependencies extends DefaultTask {
private Configuration classpath;
public CheckClasspathForProhibitedDependencies() {
getOutputs().upToDateWhen((task) -> true);
}
public void setClasspath(Configuration classpath) {
this.classpath = classpath;
}
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
@TaskAction
public void checkForProhibitedDependencies() throws IOException {
ResolvedConfiguration resolvedConfiguration = this.classpath.getResolvedConfiguration();
TreeSet<String> prohibited = resolvedConfiguration.getResolvedArtifacts().stream()
.map((artifact) -> artifact.getModuleVersion().getId()).filter(this::prohibited)
.map((id) -> id.getGroup() + ":" + id.getName()).collect(Collectors.toCollection(TreeSet::new));
if (!prohibited.isEmpty()) {
StringBuilder message = new StringBuilder(String.format("Found prohibited dependencies in '%s':%n", this.classpath.getName()));
for (String dependency : prohibited) {
message.append(String.format(" %s%n", dependency));
}
throw new GradleException(message.toString());
}
}
private boolean prohibited(ModuleVersionIdentifier id) {
String group = id.getGroup();
if (group.equals("javax.batch")) {
return false;
}
if (group.equals("javax.cache")) {
return false;
}
if (group.equals("javax.money")) {
return false;
}
if (group.startsWith("javax")) {
return true;
}
if (group.equals("commons-logging")) {
return true;
}
if (group.equals("org.slf4j") && id.getName().equals("jcl-over-slf4j")) {
return true;
}
if (group.startsWith("org.jboss.spec")) {
return true;
}
if (group.equals("org.apache.geronimo.specs")) {
return true;
}
return false;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.classpath;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.springframework.util.StringUtils;
/**
* @author Andy Wilkinson
* @author Rob Winch
*/
public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(CheckProhibitedDependenciesLifecyclePlugin.class);
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> {
configureProhibitedDependencyChecks(project);
});
}
private void configureProhibitedDependencyChecks(Project project) {
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
sourceSets.all((sourceSet) -> createProhibitedDependenciesChecks(project,
sourceSet.getCompileClasspathConfigurationName(), sourceSet.getRuntimeClasspathConfigurationName()));
}
private void createProhibitedDependenciesChecks(Project project, String... configurationNames) {
ConfigurationContainer configurations = project.getConfigurations();
for (String configurationName : configurationNames) {
Configuration configuration = configurations.getByName(configurationName);
createProhibitedDependenciesCheck(configuration, project);
}
}
private void createProhibitedDependenciesCheck(Configuration classpath, Project project) {
String taskName = "check" + StringUtils.capitalize(classpath.getName() + "ForProhibitedDependencies");
TaskProvider<CheckClasspathForProhibitedDependencies> checkClasspathTask = project.getTasks().register(taskName,
CheckClasspathForProhibitedDependencies.class, checkClasspath -> {
checkClasspath.setGroup(LifecycleBasePlugin.CHECK_TASK_NAME);
checkClasspath.setDescription("Checks " + classpath.getName() + " for prohibited dependencies");
checkClasspath.setClasspath(classpath);
});
project.getTasks().named(CheckProhibitedDependenciesLifecyclePlugin.CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, checkProhibitedTask -> checkProhibitedTask.dependsOn(checkClasspathTask));
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.classpath;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.TaskProvider;
/**
* @author Rob Winch
*/
public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin<Project> {
public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies";
@Override
public void apply(Project project) {
TaskProvider<Task> checkProhibitedDependencies = project.getTasks().register(CheckProhibitedDependenciesLifecyclePlugin.CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> {
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);
});
}
}
@@ -107,7 +107,7 @@ public class RepositoryConventionPluginTests {
this.project.getPluginManager().apply(RepositoryConventionPlugin.class);
RepositoryHandler repositories = this.project.getRepositories();
assertThat(repositories).hasSize(5);
assertThat(repositories).hasSize(4);
assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal");
}
@@ -119,39 +119,33 @@ public class RepositoryConventionPluginTests {
this.project.getPluginManager().apply(RepositoryConventionPlugin.class);
RepositoryHandler repositories = this.project.getRepositories();
assertThat(repositories).hasSize(6);
assertThat(repositories).hasSize(5);
assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal");
}
private void assertSnapshotRepository(RepositoryHandler repositories) {
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(6);
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
.isEqualTo("https://repo.maven.apache.org/maven2/");
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
.isEqualTo("https://jcenter.bintray.com/");
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
.isEqualTo("https://repo.spring.io/snapshot/");
assertThat(((MavenArtifactRepository) repositories.get(3)).getUrl().toString())
.isEqualTo("https://repo.spring.io/milestone/");
}
private void assertMilestoneRepository(RepositoryHandler repositories) {
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(5);
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
.isEqualTo("https://repo.maven.apache.org/maven2/");
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
.isEqualTo("https://jcenter.bintray.com/");
.isEqualTo("https://repo.spring.io/snapshot/");
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
.isEqualTo("https://repo.spring.io/milestone/");
}
private void assertReleaseRepository(RepositoryHandler repositories) {
private void assertMilestoneRepository(RepositoryHandler repositories) {
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(4);
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
.isEqualTo("https://repo.maven.apache.org/maven2/");
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
.isEqualTo("https://jcenter.bintray.com/");
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
.isEqualTo("https://repo.spring.io/milestone/");
}
private void assertReleaseRepository(RepositoryHandler repositories) {
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(3);
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
.isEqualTo("https://repo.maven.apache.org/maven2/");
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
.isEqualTo("https://repo.spring.io/release/");
}
@@ -58,7 +58,7 @@ public class SaganApiTests {
Release release = new Release();
release.setVersion("5.6.0-SNAPSHOT");
release.setApiDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/api/");
release.setReferenceDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/");
release.setReferenceDocUrl("https://docs.spring.io/spring-security/reference/{version}/index.html");
this.sagan.createReleaseForProject(release, "spring-security");
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases");
@@ -67,7 +67,7 @@ public class SaganApiTests {
assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" +
" \"version\":\"5.6.0-SNAPSHOT\",\n" +
" \"current\":false,\n" +
" \"referenceDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/\",\n" +
" \"referenceDocUrl\":\"https://docs.spring.io/spring-security/reference/{version}/index.html\",\n" +
" \"apiDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/api/\"\n" +
"}");
}
@@ -0,0 +1,270 @@
package org.springframework.gradle.antora;
import org.apache.commons.io.IOUtils;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.testfixtures.ProjectBuilder;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIOException;
class CheckAntoraVersionPluginTests {
@Test
void defaultsPropertiesWhenSnapshot() {
String expectedVersion = "1.0.0-SNAPSHOT";
Project project = ProjectBuilder.builder().build();
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0");
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT");
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse();
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
}
@Test
void defaultsPropertiesWhenMilestone() {
String expectedVersion = "1.0.0-M1";
Project project = ProjectBuilder.builder().build();
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-M1");
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true");
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get());
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
}
@Test
void defaultsPropertiesWhenRc() {
String expectedVersion = "1.0.0-RC1";
Project project = ProjectBuilder.builder().build();
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-RC1");
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true");
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get());
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
}
@Test
void defaultsPropertiesWhenRelease() {
String expectedVersion = "1.0.0";
Project project = ProjectBuilder.builder().build();
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0");
assertThat(checkAntoraVersionTask.getAntoraPrerelease().isPresent()).isFalse();
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse();
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
}
@Test
void explicitProperties() {
Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
checkAntoraVersionTask.getAntoraVersion().set("1.0.0");
checkAntoraVersionTask.getAntoraPrerelease().set("-SNAPSHOT");
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0");
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT");
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse();
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
}
@Test
void versionNotDefined() throws Exception {
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check());
}
@Test
void antoraFileNotFound() throws Exception {
String expectedVersion = "1.0.0-SNAPSHOT";
Project project = ProjectBuilder.builder().build();
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThatIOException().isThrownBy(() -> checkAntoraVersionTask.check());
}
@Test
void actualAntoraPrereleaseNull() throws Exception {
String expectedVersion = "1.0.0-SNAPSHOT";
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check());
}
@Test
void matchesWhenSnapshot() throws Exception {
String expectedVersion = "1.0.0-SNAPSHOT";
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
checkAntoraVersionTask.check();
}
@Test
void matchesWhenMilestone() throws Exception {
String expectedVersion = "1.0.0-M1";
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'\ndisplay_version: '1.0.0-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
checkAntoraVersionTask.check();
}
@Test
void matchesWhenRc() throws Exception {
String expectedVersion = "1.0.0-RC1";
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'\ndisplay_version: '1.0.0-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
checkAntoraVersionTask.check();
}
@Test
void matchesWhenReleaseAndPrereleaseUndefined() throws Exception {
String expectedVersion = "1.0.0";
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.setVersion(expectedVersion);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
checkAntoraVersionTask.check();
}
@Test
void matchesWhenExplicitRelease() throws Exception {
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0");
checkAntoraVersionTask.check();
}
@Test
void matchesWhenExplicitPrerelease() throws Exception {
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0");
((CheckAntoraVersionTask) task).getAntoraPrerelease().set("-SNAPSHOT");
checkAntoraVersionTask.check();
}
@Test
void matchesWhenMissingPropertyDefined() throws Exception {
Project project = ProjectBuilder.builder().build();
File rootDir = project.getRootDir();
IOUtils.write("name: 'ROOT'\nversion: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8);
project.getPluginManager().apply(CheckAntoraVersionPlugin.class);
Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0");
checkAntoraVersionTask.check();
}
}
@@ -9,6 +9,6 @@ repositories {
}
dependencies {
optional 'javax.servlet:javax.servlet-api:3.1.0'
optional 'jakarta.servlet:jakarta.servlet-api:3.1.0'
testCompile 'junit:junit:4.12'
}
+5 -2
View File
@@ -4,7 +4,10 @@ dependencies {
management platform(project(":spring-security-dependencies"))
api project(':spring-security-core')
api project(':spring-security-web')
api 'org.jasig.cas.client:cas-client-core'
api('org.jasig.cas.client:cas-client-core') {
exclude group: 'org.glassfish.jaxb', module: 'jaxb-core'
exclude group: 'javax.xml.bind', module: 'jaxb-api'
}
api 'org.springframework:spring-beans'
api 'org.springframework:spring-context'
api 'org.springframework:spring-core'
@@ -13,7 +16,7 @@ dependencies {
optional 'com.fasterxml.jackson.core:jackson-databind'
optional 'net.sf.ehcache:ehcache'
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet:jakarta.servlet-api'
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
+13 -6
View File
@@ -37,9 +37,9 @@ dependencies {
optional'org.springframework:spring-websocket'
optional 'org.jetbrains.kotlin:kotlin-reflect'
optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
optional 'javax.annotation:jsr250-api'
optional 'jakarta.annotation:jakarta.annotation-api'
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet:jakarta.servlet-api'
testImplementation project(':spring-security-aspects')
testImplementation project(':spring-security-cas')
@@ -62,8 +62,10 @@ dependencies {
testImplementation 'ch.qos.logback:logback-classic'
testImplementation 'io.projectreactor.netty:reactor-netty'
testImplementation 'io.rsocket:rsocket-transport-netty'
testImplementation 'javax.annotation:jsr250-api:1.0'
testImplementation 'javax.xml.bind:jaxb-api'
testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0'
testImplementation "jakarta.inject:jakarta.inject-api"
testImplementation "jakarta.transaction:jakarta.transaction-api"
testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api'
testImplementation 'ldapsdk:ldapsdk:4.1'
testImplementation('net.sourceforge.htmlunit:htmlunit') {
exclude group: 'commons-logging', module: 'commons-logging'
@@ -75,12 +77,18 @@ dependencies {
testImplementation "org.apache.directory.server:apacheds-server-jndi"
testImplementation 'org.apache.directory.shared:shared-ldap'
testImplementation 'org.eclipse.persistence:javax.persistence'
testImplementation 'org.hibernate:hibernate-entitymanager'
testImplementation('org.hibernate:hibernate-entitymanager') {
exclude group: 'javax.activation', module: 'javax.activation-api'
exclude group: 'javax.persistence', module: 'javax.persistence-api'
exclude group: 'javax.xml.bind', module: 'jaxb-api'
exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.2_spec'
}
testImplementation 'org.hsqldb:hsqldb'
testImplementation 'org.mockito:mockito-core'
testImplementation "org.mockito:mockito-inline"
testImplementation ('org.openid4java:openid4java-nodeps') {
exclude group: 'com.google.code.guice', module: 'guice'
exclude group: 'commons-logging', module: 'commons-logging'
}
testImplementation('org.seleniumhq.selenium:htmlunit-driver') {
exclude group: 'commons-logging', module: 'commons-logging'
@@ -89,7 +97,6 @@ dependencies {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'io.netty', module: 'netty'
}
testImplementation 'org.slf4j:jcl-over-slf4j'
testImplementation 'org.springframework.ldap:spring-ldap-core'
testImplementation 'org.springframework:spring-expression'
testImplementation 'org.springframework:spring-jdbc'
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
@@ -33,6 +34,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityBuilder;
@@ -47,9 +49,12 @@ import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.firewall.HttpFirewall;
@@ -57,7 +62,9 @@ import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.util.Assert;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.filter.DelegatingFilterProxy;
/**
@@ -81,7 +88,7 @@ import org.springframework.web.filter.DelegatingFilterProxy;
* @see WebSecurityConfiguration
*/
public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity>
implements SecurityBuilder<Filter>, ApplicationContextAware {
implements SecurityBuilder<Filter>, ApplicationContextAware, ServletContextAware {
private final Log logger = LogFactory.getLog(getClass());
@@ -108,6 +115,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
private Runnable postBuildAction = () -> {
};
private ServletContext servletContext;
/**
* Creates a new instance
* @param objectPostProcessor the {@link ObjectPostProcessor} to use
@@ -252,6 +261,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
* {@link WebSecurityConfigurerAdapter}.
* @param securityInterceptor the {@link FilterSecurityInterceptor} to use
* @return the {@link WebSecurity} for further customizations
* @deprecated Use {@link #privilegeEvaluator(WebInvocationPrivilegeEvaluator)}
* instead
*/
public WebSecurity securityInterceptor(FilterSecurityInterceptor securityInterceptor) {
this.filterSecurityInterceptor = securityInterceptor;
@@ -278,11 +289,24 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
+ ".addSecurityFilterChainBuilder directly");
int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
for (RequestMatcher ignoredRequest : this.ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
+ ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
if (this.privilegeEvaluator == null) {
this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
requestMatcherPrivilegeEvaluatorsEntries);
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (this.httpFirewall != null) {
@@ -306,6 +330,26 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
return result;
}
private RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> getRequestMatcherPrivilegeEvaluatorsEntry(
SecurityFilterChain securityFilterChain) {
List<WebInvocationPrivilegeEvaluator> privilegeEvaluators = new ArrayList<>();
for (Filter filter : securityFilterChain.getFilters()) {
if (filter instanceof FilterSecurityInterceptor) {
DefaultWebInvocationPrivilegeEvaluator defaultWebInvocationPrivilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator(
(FilterSecurityInterceptor) filter);
defaultWebInvocationPrivilegeEvaluator.setServletContext(this.servletContext);
privilegeEvaluators.add(defaultWebInvocationPrivilegeEvaluator);
continue;
}
if (filter instanceof AuthorizationFilter) {
AuthorizationManager<HttpServletRequest> authorizationManager = ((AuthorizationFilter) filter)
.getAuthorizationManager();
privilegeEvaluators.add(new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager));
}
}
return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext);
@@ -333,6 +377,11 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
}
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
* An {@link IgnoredRequestConfigurer} that allows optionally configuring the
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
@@ -128,8 +128,8 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa
}
/**
* Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP
* tag support.
* Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary to evaluate
* privileges for a given web URI
* @return the {@link WebInvocationPrivilegeEvaluator}
*/
@Bean
@@ -297,8 +297,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
if (version != null) {
return version;
}
return Version.class.getModule().getDescriptor().version().map(Object::toString)
.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
return Version.getVersion();
}
private void registerDefaultAuthenticationProvider(B http) {
@@ -319,8 +319,7 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
if (version != null) {
return version;
}
return Version.class.getModule().getDescriptor().version().map(Object::toString)
.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
return Version.getVersion();
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ 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.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
@@ -62,7 +63,7 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.test.web.servlet.MockMvc;
@@ -84,6 +85,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* @author Rob Winch
* @author Joe Grandja
* @author Evgeniy Cheban
* @author Marcus Da Coregio
*/
@ExtendWith(SpringTestContextExtension.class)
public class WebSecurityConfigurationTests {
@@ -218,10 +220,10 @@ public class WebSecurityConfigurationTests {
}
@Test
public void loadConfigWhenDefaultWebInvocationPrivilegeEvaluatorThenDefaultIsRegistered() {
public void loadConfigWhenDefaultWebInvocationPrivilegeEvaluatorThenRequestMatcherIsRegistered() {
this.spring.register(WebInvocationPrivilegeEvaluatorDefaultsConfig.class).autowire();
assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
.isInstanceOf(DefaultWebInvocationPrivilegeEvaluator.class);
.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
}
@Test
@@ -229,7 +231,7 @@ public class WebSecurityConfigurationTests {
this.spring.register(AuthorizeRequestsFilterChainConfig.class).autowire();
assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
.isInstanceOf(DefaultWebInvocationPrivilegeEvaluator.class);
.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
}
// SEC-2303
@@ -375,6 +377,69 @@ public class WebSecurityConfigurationTests {
assertThat(filterChains.get(1).matches(request)).isTrue();
}
@Test
public void loadConfigWhenTwoSecurityFilterChainsThenRequestMatcherDelegatingWebInvocationPrivilegeEvaluator() {
this.spring.register(TwoSecurityFilterChainConfig.class).autowire();
assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
}
@Test
public void loadConfigWhenTwoSecurityFilterChainDebugThenRequestMatcherDelegatingWebInvocationPrivilegeEvaluator() {
this.spring.register(TwoSecurityFilterChainConfig.class).autowire();
assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
}
// gh-10554
@Test
public void loadConfigWhenMultipleSecurityFilterChainsThenWebInvocationPrivilegeEvaluatorApplySecurity() {
this.spring.register(MultipleSecurityFilterChainConfig.class).autowire();
WebInvocationPrivilegeEvaluator privilegeEvaluator = this.spring.getContext()
.getBean(WebInvocationPrivilegeEvaluator.class);
assertUserPermissions(privilegeEvaluator);
assertAdminPermissions(privilegeEvaluator);
assertAnotherUserPermission(privilegeEvaluator);
}
// gh-10554
@Test
public void loadConfigWhenMultipleSecurityFilterChainAndIgnoringThenWebInvocationPrivilegeEvaluatorAcceptsNullAuthenticationOnIgnored() {
this.spring.register(MultipleSecurityFilterChainIgnoringConfig.class).autowire();
WebInvocationPrivilegeEvaluator privilegeEvaluator = this.spring.getContext()
.getBean(WebInvocationPrivilegeEvaluator.class);
assertUserPermissions(privilegeEvaluator);
assertAdminPermissions(privilegeEvaluator);
assertAnotherUserPermission(privilegeEvaluator);
// null authentication
assertThat(privilegeEvaluator.isAllowed("/user", null)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/admin", null)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/another", null)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/ignoring1", null)).isTrue();
assertThat(privilegeEvaluator.isAllowed("/ignoring1/child", null)).isTrue();
}
private void assertAnotherUserPermission(WebInvocationPrivilegeEvaluator privilegeEvaluator) {
Authentication anotherUser = new TestingAuthenticationToken("anotherUser", "password", "ROLE_ANOTHER");
assertThat(privilegeEvaluator.isAllowed("/user", anotherUser)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/admin", anotherUser)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/another", anotherUser)).isTrue();
}
private void assertAdminPermissions(WebInvocationPrivilegeEvaluator privilegeEvaluator) {
Authentication admin = new TestingAuthenticationToken("admin", "password", "ROLE_ADMIN");
assertThat(privilegeEvaluator.isAllowed("/user", admin)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/admin", admin)).isTrue();
assertThat(privilegeEvaluator.isAllowed("/another", admin)).isTrue();
}
private void assertUserPermissions(WebInvocationPrivilegeEvaluator privilegeEvaluator) {
Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
assertThat(privilegeEvaluator.isAllowed("/user", user)).isTrue();
assertThat(privilegeEvaluator.isAllowed("/admin", user)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/another", user)).isTrue();
}
@EnableWebSecurity
@Import(AuthenticationTestConfiguration.class)
static class SortedWebSecurityConfigurerAdaptersConfig {
@@ -1008,4 +1073,125 @@ public class WebSecurityConfigurationTests {
}
@EnableWebSecurity
static class TwoSecurityFilterChainConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain path1(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers((requests) -> requests.antMatchers("/path1/**"))
.authorizeRequests((requests) -> requests.anyRequest().authenticated());
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
return http.build();
}
}
@EnableWebSecurity(debug = true)
static class TwoSecurityFilterChainDebugConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain path1(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers((requests) -> requests.antMatchers("/path1/**"))
.authorizeRequests((requests) -> requests.anyRequest().authenticated());
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
return http.build();
}
}
@EnableWebSecurity
@Import(AuthenticationTestConfiguration.class)
static class MultipleSecurityFilterChainConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers((requests) -> requests.antMatchers("/user"))
.authorizeRequests((requests) -> requests.anyRequest().hasRole("USER"));
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public SecurityFilterChain path1(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers((requests) -> requests.antMatchers("/admin"))
.authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN"));
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
return http.build();
}
}
@EnableWebSecurity
@Import(AuthenticationTestConfiguration.class)
static class MultipleSecurityFilterChainIgnoringConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/ignoring1/**");
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers((requests) -> requests.antMatchers("/user"))
.authorizeRequests((requests) -> requests.anyRequest().hasRole("USER"));
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public SecurityFilterChain admin(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers((requests) -> requests.antMatchers("/admin"))
.authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN"));
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
return http.build();
}
}
}
@@ -21,11 +21,13 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletRequest;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import sun.security.x509.X500Name;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -240,12 +242,8 @@ public class NamespaceHttpX509Tests {
}
private String extractCommonName(X509Certificate certificate) {
try {
return ((X500Name) certificate.getSubjectDN()).getCommonName();
}
catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
X500Principal principal = certificate.getSubjectX500Principal();
return new X500Name(principal.getName()).getRDNs(BCStyle.CN)[0].getFirst().getValue().toString();
}
}
@@ -253,7 +253,7 @@ public class Saml2LoginConfigurerTests {
public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses() throws Exception {
this.spring.register(CustomAuthenticationFailureHandler.class).autowire();
byte[] invalidDeflated = "invalid".getBytes();
String encoded = Saml2Utils.samlEncode(invalidDeflated);
String encoded = Saml2Utils.samlEncodeNotRfc2045(invalidDeflated);
MockHttpServletRequestBuilder request = get("/login/saml2/sso/registration-id").queryParam("SAMLResponse",
encoded);
this.mvc.perform(request);
+1 -2
View File
@@ -13,7 +13,7 @@ dependencies {
optional 'com.fasterxml.jackson.core:jackson-databind'
optional 'io.projectreactor:reactor-core'
optional 'javax.annotation:jsr250-api'
optional 'jakarta.annotation:jakarta.annotation-api'
optional 'net.sf.ehcache:ehcache'
optional 'org.aspectj:aspectjrt'
optional 'org.springframework:spring-jdbc'
@@ -31,7 +31,6 @@ dependencies {
testImplementation "org.mockito:mockito-junit-jupiter"
testImplementation "org.springframework:spring-test"
testImplementation 'org.skyscreamer:jsonassert'
testImplementation 'org.slf4j:jcl-over-slf4j'
testImplementation 'org.springframework:spring-test'
testRuntimeOnly 'org.hsqldb:hsqldb'
@@ -133,8 +133,10 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
private boolean isAuthorized(Authentication authentication) {
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (this.authorities.contains(grantedAuthority)) {
return true;
for (GrantedAuthority authority : this.authorities) {
if (authority.getAuthority().equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
return false;
@@ -45,9 +45,10 @@ public class AuthorityReactiveAuthorizationManager<T> implements ReactiveAuthori
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
// @formatter:off
return authentication.filter((a) -> a.isAuthenticated())
return authentication.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.any(this.authorities::contains)
.map(GrantedAuthority::getAuthority)
.any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> authority.getAuthority().equals(grantedAuthority)))
.map((granted) -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(granted, this.authorities)))
.defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities));
// @formatter:on
@@ -16,12 +16,14 @@
package org.springframework.security.authorization;
import java.util.Collections;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -133,6 +135,30 @@ public class AuthorityAuthorizationManagerTests {
assertThat(manager.check(authentication, object).isGranted()).isFalse();
}
@Test
public void hasAuthorityWhenUserHasCustomAuthorityThenGrantedDecision() {
AuthorityAuthorizationManager<Object> manager = AuthorityAuthorizationManager.hasAuthority("ADMIN");
GrantedAuthority customGrantedAuthority = () -> "ADMIN";
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
Collections.singletonList(customGrantedAuthority));
Object object = new Object();
assertThat(manager.check(authentication, object).isGranted()).isTrue();
}
@Test
public void hasAuthorityWhenUserHasNotCustomAuthorityThenDeniedDecision() {
AuthorityAuthorizationManager<Object> manager = AuthorityAuthorizationManager.hasAuthority("ADMIN");
GrantedAuthority customGrantedAuthority = () -> "USER";
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
Collections.singletonList(customGrantedAuthority));
Object object = new Object();
assertThat(manager.check(authentication, object).isGranted()).isFalse();
}
@Test
public void hasAnyRoleWhenUserHasAnyRoleThenGrantedDecision() {
AuthorityAuthorizationManager<Object> manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER");
@@ -27,6 +27,7 @@ import reactor.test.StepVerifier;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -88,6 +89,24 @@ public class AuthorityReactiveAuthorizationManagerTests {
assertThat(granted).isTrue();
}
@Test
public void checkWhenHasCustomAuthorityAndAuthorizedThenReturnTrue() {
GrantedAuthority customGrantedAuthority = () -> "ADMIN";
this.authentication = new TestingAuthenticationToken("rob", "secret",
Collections.singletonList(customGrantedAuthority));
boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted();
assertThat(granted).isTrue();
}
@Test
public void checkWhenHasCustomAuthorityAndAuthenticatedAndWrongAuthoritiesThenReturnFalse() {
GrantedAuthority customGrantedAuthority = () -> "USER";
this.authentication = new TestingAuthenticationToken("rob", "secret",
Collections.singletonList(customGrantedAuthority));
boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted();
assertThat(granted).isFalse();
}
@Test
public void checkWhenHasRoleAndAuthorizedThenReturnTrue() {
this.manager = AuthorityReactiveAuthorizationManager.hasRole("ADMIN");
@@ -18,6 +18,7 @@ package org.springframework.security.core;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -60,15 +61,24 @@ public class SpringSecurityCoreVersionTests {
@BeforeEach
public void setup() throws Exception {
Field logger = ReflectionUtils.findField(SpringSecurityCoreVersion.class, "logger");
StaticFinalReflectionUtils.setField(logger, this.logger);
setFinalStaticField(SpringSecurityCoreVersion.class, "logger", this.logger);
}
@AfterEach
public void cleanup() throws Exception {
System.clearProperty(getDisableChecksProperty());
Field logger = ReflectionUtils.findField(SpringSecurityCoreVersion.class, "logger");
StaticFinalReflectionUtils.setField(logger, LogFactory.getLog(SpringSecurityCoreVersion.class));
setFinalStaticField(SpringSecurityCoreVersion.class, "logger",
LogFactory.getLog(SpringSecurityCoreVersion.class));
}
private static void setFinalStaticField(Class<?> clazz, String fieldName, Object value)
throws ReflectiveOperationException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, value);
}
@Test
@@ -1,115 +0,0 @@
/*
* Copyright 2008 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.core;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import sun.misc.Unsafe;
import org.springframework.objenesis.instantiator.util.UnsafeUtils;
/**
* Used for setting static variables even if they are private static final.
*
* The code in this class has been adopted from Powermock's <a href=
* "https://github.com/noushadali/powermock/blob/powermock-1.5.4/reflect/src/main/java/org/powermock/reflect/internal/WhiteboxImpl.java#L326">WhiteboxImpl</a>.
*
* @author Rob Winch
*/
final class StaticFinalReflectionUtils {
/**
* Used to support setting static fields that are final using Java's Unsafe. If the
* field is not static final, use
* {@link org.springframework.test.util.ReflectionTestUtils}.
* @param field the field to set
* @param newValue the new value
*/
static void setField(final Field field, final Object newValue) {
try {
field.setAccessible(true);
int fieldModifiersMask = field.getModifiers();
boolean isFinalModifierPresent = (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL;
if (isFinalModifierPresent) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
Unsafe unsafe = UnsafeUtils.getUnsafe();
long offset = unsafe.staticFieldOffset(field);
Object base = unsafe.staticFieldBase(field);
setFieldUsingUnsafe(base, field.getType(), offset, newValue, unsafe);
return null;
}
catch (Throwable thrown) {
throw new RuntimeException(thrown);
}
}
});
}
else {
field.set(null, newValue);
}
}
catch (SecurityException ex) {
throw new RuntimeException(ex);
}
catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
catch (IllegalArgumentException ex) {
throw new RuntimeException(ex);
}
}
private static void setFieldUsingUnsafe(Object base, Class type, long offset, Object newValue, Unsafe unsafe) {
if (type == Integer.TYPE) {
unsafe.putInt(base, offset, ((Integer) newValue));
}
else if (type == Short.TYPE) {
unsafe.putShort(base, offset, ((Short) newValue));
}
else if (type == Long.TYPE) {
unsafe.putLong(base, offset, ((Long) newValue));
}
else if (type == Byte.TYPE) {
unsafe.putByte(base, offset, ((Byte) newValue));
}
else if (type == Boolean.TYPE) {
unsafe.putBoolean(base, offset, ((Boolean) newValue));
}
else if (type == Float.TYPE) {
unsafe.putFloat(base, offset, ((Float) newValue));
}
else if (type == Double.TYPE) {
unsafe.putDouble(base, offset, ((Double) newValue));
}
else if (type == Character.TYPE) {
unsafe.putChar(base, offset, ((Character) newValue));
}
else {
unsafe.putObject(base, offset, newValue);
}
}
private StaticFinalReflectionUtils() {
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ apply plugin: 'io.spring.convention.spring-module'
dependencies {
management platform(project(":spring-security-dependencies"))
api project(':spring-security-core')
api 'javax.xml.bind:jaxb-api'
api 'jakarta.xml.bind:jakarta.xml.bind-api'
api 'org.springframework.data:spring-data-commons'
api 'org.springframework:spring-core'
+21 -21
View File
@@ -8,15 +8,15 @@ javaPlatform {
dependencies {
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
api platform("io.projectreactor:reactor-bom:2020.0.13")
api platform("io.rsocket:rsocket-bom:1.1.1")
api platform("org.junit:junit-bom:5.8.1")
api platform("org.springframework.data:spring-data-bom:2021.1.0")
api platform("io.projectreactor:reactor-bom:2020.0.18")
api platform("io.rsocket:rsocket-bom:1.1.2")
api platform("org.junit:junit-bom:5.8.2")
api platform("org.springframework.data:spring-data-bom:2021.1.3")
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2")
api platform("com.fasterxml.jackson:jackson-bom:2.13.0")
api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328")
constraints {
api "ch.qos.logback:logback-classic:1.2.7"
api "ch.qos.logback:logback-classic:1.2.11"
api "com.google.inject:guice:3.0"
api "com.nimbusds:nimbus-jose-jwt:9.14"
api "com.nimbusds:oauth2-oidc-sdk:9.19"
@@ -25,14 +25,15 @@ dependencies {
api "com.unboundid:unboundid-ldapsdk:4.0.14"
api "commons-codec:commons-codec:1.15"
api "commons-collections:commons-collections:3.2.2"
api "commons-logging:commons-logging:1.2"
api "io.mockk:mockk:1.12.0"
api "io.mockk:mockk:1.12.3"
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
api "javax.annotation:jsr250-api:1.0"
api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2"
api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3"
api "javax.servlet:javax.servlet-api:4.0.1"
api "javax.xml.bind:jaxb-api:2.3.1"
api "jakarta.inject:jakarta.inject-api:1.0.5"
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7"
api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6"
api "jakarta.servlet:jakarta.servlet-api:4.0.4"
api "jakarta.transaction:jakarta.transaction-api:1.3.3"
api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3"
api "ldapsdk:ldapsdk:4.1"
api "net.sf.ehcache:ehcache:2.10.9.2"
api "net.sourceforge.htmlunit:htmlunit:2.54.0"
@@ -49,13 +50,13 @@ dependencies {
api "org.assertj:assertj-core:3.21.0"
api "org.bouncycastle:bcpkix-jdk15on:1.69"
api "org.bouncycastle:bcprov-jdk15on:1.69"
api "org.eclipse.jetty:jetty-server:9.4.44.v20210927"
api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927"
api "org.eclipse.jetty:jetty-server:9.4.46.v20220331"
api "org.eclipse.jetty:jetty-servlet:9.4.46.v20220331"
api "org.eclipse.persistence:javax.persistence:2.2.1"
api "org.hamcrest:hamcrest:2.2"
api "org.hibernate:hibernate-entitymanager:5.6.1.Final"
api "org.hibernate:hibernate-entitymanager:5.6.8.Final"
api "org.hsqldb:hsqldb:2.6.1"
api "org.jasig.cas.client:cas-client-core:3.6.2"
api "org.jasig.cas.client:cas-client-core:3.6.4"
api "org.mockito:mockito-core:3.12.4"
api "org.mockito:mockito-inline:3.12.4"
api "org.mockito:mockito-junit-jupiter:3.12.4"
@@ -68,10 +69,9 @@ dependencies {
api "org.seleniumhq.selenium:selenium-java:3.141.59"
api "org.seleniumhq.selenium:selenium-support:3.141.59"
api "org.skyscreamer:jsonassert:1.5.0"
api "org.slf4j:jcl-over-slf4j:1.7.32"
api "org.slf4j:log4j-over-slf4j:1.7.32"
api "org.slf4j:slf4j-api:1.7.32"
api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE"
api "org.slf4j:log4j-over-slf4j:1.7.36"
api "org.slf4j:slf4j-api:1.7.36"
api "org.springframework.ldap:spring-ldap-core:2.3.7.RELEASE"
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
name: ROOT
version: '5.6.0'
version: '5.6.3'
Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

+3 -1
View File
@@ -49,7 +49,8 @@
*** xref:servlet/authentication/events.adoc[Authentication Events]
** xref:servlet/authorization/index.adoc[Authorization]
*** xref:servlet/authorization/architecture.adoc[Authorization Architecture]
*** xref:servlet/authorization/authorize-requests.adoc[Authorize HTTP Requests]
*** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]
*** xref:servlet/authorization/authorize-requests.adoc[Authorize HTTP Requests with FilterSecurityInterceptor]
*** xref:servlet/authorization/expression-based.adoc[Expression-Based Access Control]
*** xref:servlet/authorization/secure-objects.adoc[Secure Object Implementations]
*** xref:servlet/authorization/method-security.adoc[Method Security]
@@ -123,6 +124,7 @@
*** xref:reactive/authentication/x509.adoc[X.509 Authentication]
*** xref:reactive/authentication/logout.adoc[Logout]
** Authorization
*** xref:reactive/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]
*** xref:reactive/authorization/method.adoc[EnableReactiveMethodSecurity]
** xref:reactive/oauth2/index.adoc[OAuth2]
*** xref:reactive/oauth2/login/index.adoc[OAuth2 Log In]
@@ -463,7 +463,7 @@ You should instead migrate to using `DelegatingPasswordEncoder` to support secur
[source,java,role="primary"]
----
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
----
@@ -198,10 +198,13 @@ The password package of the spring-security-crypto module provides support for e
[source,java]
----
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
String encode(String rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
boolean matches(String rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
----
@@ -53,7 +53,7 @@ If you wish to override the Spring Security version, you may do so by providing
<properties>
<!-- ... -->
<spring-security.version>{spring-security-version}</spring-security.version>
</dependencies>
</properties>
----
====
@@ -68,7 +68,7 @@ You can do so by adding a Maven property, as the following example shows:
<properties>
<!-- ... -->
<spring.version>{spring-core-version}</spring.version>
</dependencies>
</properties>
----
====
@@ -0,0 +1,104 @@
[[servlet-authorization-authorizationfilter]]
= Authorize ServerHttpRequest
Spring Security provides support for authorizing the incoming HTTP requests.
By default, Spring Securitys authorization will require all requests to be authenticated.
The explicit configuration looks like:
.All Requests Require Authenticated User
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin { }
httpBasic { }
}
}
----
====
We can configure Spring Security to have different rules by adding more rules in order of precedence.
.Multiple Authorize Requests Rules
====
.Java
[source,java,role="primary"]
----
import static org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.hasRole;
// ...
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
// ...
.authorizeExchange((authorize) -> authorize // <1>
.pathMatchers("/resources/**", "/signup", "/about").permitAll() // <2>
.pathMatchers("/admin/**").hasRole("ADMIN") // <3>
.pathMatchers("/db/**").access((authentication, context) -> // <4>
hasRole("ADMIN").check(authentication, context)
.filter(decision -> !decision.isGranted())
.switchIfEmpty(hasRole("DBA").check(authentication, context))
)
.anyExchange().denyAll() // <5>
);
// @formatter:on
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange { // <1>
authorize(pathMatchers("/resources/**", "/signup", "/about"), permitAll) // <2>
authorize("/admin/**", hasRole("ADMIN")) // <3>
authorize("/db/**", { authentication, context -> // <4>
hasRole("ADMIN").check(authentication, context)
.filter({ decision -> !decision.isGranted() })
.switchIfEmpty(hasRole("DBA").check(authentication, context))
})
authorize(anyExchange, denyAll) // <5>
}
// ...
}
}
----
====
<1> There are multiple authorization rules specified.
Each rule is considered in the order they were declared.
<2> We specified multiple URL patterns that any user can access.
Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about".
<3> Any URL that starts with "/admin/" will be restricted to users who have the authority "ROLE_ADMIN".
You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix.
<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA".
This demonstrates the flexibility of providing a custom `ReactiveAuthorizationManager` allowing us to implement arbitrary authorization logic.
For simplicity, the sample uses a lambda and delegate to the existing `AuthorityReactiveAuthorizationManager.hasRole` implementation.
However, in a real world situation applications would likely implement the logic in a proper class implementing `ReactiveAuthorizationManager`.
<5> Any URL that has not already been matched on is denied access.
This is a good strategy if you do not want to accidentally forget to update your authorization rules.
@@ -162,7 +162,7 @@ class SecurityConfig {
@Bean
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
@@ -170,8 +170,6 @@ class SecurityConfig {
authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository)
}
}
return http.build()
}
private fun authorizationRequestResolver(
@@ -282,13 +280,11 @@ class OAuth2ClientSecurityConfig {
@Bean
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Client {
authorizationRequestRepository = authorizationRequestRepository()
}
}
return http.build()
}
}
----
@@ -363,13 +359,11 @@ class OAuth2ClientSecurityConfig {
@Bean
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Client {
authenticationManager = authorizationCodeAuthenticationManager()
}
}
return http.build()
}
private fun authorizationCodeAuthenticationManager(): ReactiveAuthenticationManager {
@@ -55,7 +55,7 @@ class OAuth2ClientSecurityConfig {
@Bean
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Client {
clientRegistrationRepository = clientRegistrationRepository()
authorizedClientRepository = authorizedClientRepository()
@@ -64,8 +64,6 @@ class OAuth2ClientSecurityConfig {
authenticationManager = authenticationManager()
}
}
return http.build()
}
}
----
@@ -60,7 +60,7 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Login {
authenticationConverter = authenticationConverter()
authenticationMatcher = authenticationMatcher()
@@ -75,8 +75,6 @@ class OAuth2LoginSecurityConfig {
securityContextRepository = securityContextRepository()
}
}
return http.build()
}
}
----
@@ -158,7 +156,7 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
exceptionHandling {
authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2")
}
@@ -166,8 +164,6 @@ class OAuth2LoginSecurityConfig {
authorizationRequestResolver = authorizationRequestResolver()
}
}
return http.build()
}
private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver {
@@ -243,13 +239,11 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Login {
authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")
}
}
return http.build()
}
}
----
@@ -369,11 +363,9 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Login { }
}
return http.build()
}
@Bean
@@ -458,11 +450,9 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Login { }
}
return http.build()
}
@Bean
@@ -536,11 +526,9 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Login { }
}
return http.build()
}
@Bean
@@ -594,11 +582,9 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
oauth2Login { }
}
return http.build()
}
@Bean
@@ -730,7 +716,7 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
@@ -739,8 +725,6 @@ class OAuth2LoginSecurityConfig {
logoutSuccessHandler = oidcLogoutSuccessHandler()
}
}
return http.build()
}
private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler {
@@ -341,14 +341,12 @@ class OAuth2LoginSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login { }
}
return http.build()
}
}
----
@@ -411,14 +409,12 @@ class OAuth2LoginConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login { }
}
return http.build()
}
@Bean
@@ -505,14 +501,12 @@ class OAuth2LoginConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login { }
}
return http.build()
}
@Bean
@@ -19,8 +19,6 @@ This also gives a good idea of the high level flow of authentication and how pie
[[servlet-authentication-securitycontextholder]]
== SecurityContextHolder
Hi {figures} there
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
It contains the <<servlet-authentication-securitycontext>>.
@@ -8,7 +8,7 @@
== Authorities
xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`], discusses how all `Authentication` implementations store a list of `GrantedAuthority` objects.
These represent the authorities that have been granted to the principal.
The `GrantedAuthority` objects are inserted into the `Authentication` object by the `AuthenticationManager` and are later read by ``AccessDecisionManager``s when making authorization decisions.
The `GrantedAuthority` objects are inserted into the `Authentication` object by the `AuthenticationManager` and are later read by either the `AuthorizationManager` when making authorization decisions.
`GrantedAuthority` is an interface with only one method:
@@ -19,25 +19,219 @@ String getAuthority();
----
This method allows
``AccessDecisionManager``s to obtain a precise `String` representation of the `GrantedAuthority`.
By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most ``AccessDecisionManager``s.
This method allows ``AuthorizationManager``s to obtain a precise `String` representation of the `GrantedAuthority`.
By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most ``AuthorizationManager``s and ``AccessDecisionManager``s.
If a `GrantedAuthority` cannot be precisely represented as a `String`, the `GrantedAuthority` is considered "complex" and `getAuthority()` must return `null`.
An example of a "complex" `GrantedAuthority` would be an implementation that stores a list of operations and authority thresholds that apply to different customer account numbers.
Representing this complex `GrantedAuthority` as a `String` would be quite difficult, and as a result the `getAuthority()` method should return `null`.
This will indicate to any `AccessDecisionManager` that it will need to specifically support the `GrantedAuthority` implementation in order to understand its contents.
This will indicate to any `AuthorizationManager` that it will need to specifically support the `GrantedAuthority` implementation in order to understand its contents.
Spring Security includes one concrete `GrantedAuthority` implementation, `SimpleGrantedAuthority`.
This allows any user-specified `String` to be converted into a `GrantedAuthority`.
All ``AuthenticationProvider``s included with the security architecture use `SimpleGrantedAuthority` to populate the `Authentication` object.
[[authz-pre-invocation]]
== Pre-Invocation Handling
Spring Security provides interceptors which control access to secure objects such as method invocations or web requests.
A pre-invocation decision on whether the invocation is allowed to proceed is made by the `AccessDecisionManager`.
=== The AuthorizationManager
`AuthorizationManager` supersedes both <<authz-legacy-note,`AccessDecisionManager` and `AccessDecisionVoter`>>.
Applications that customize an `AccessDecisionManager` or `AccessDecisionVoter` are encouraged to <<authz-voter-adaptation,change to using `AuthorizationManager`>>.
``AuthorizationManager``s are called by the xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] and are responsible for making final access control decisions.
The `AuthorizationManager` interface contains two methods:
[source,java]
----
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
// ...
}
----
The ``AuthorizationManager``'s `check` method is passed all the relevant information it needs in order to make an authorization decision.
In particular, passing the secure `Object` enables those arguments contained in the actual secure object invocation to be inspected.
For example, let's assume the secure object was a `MethodInvocation`.
It would be easy to query the `MethodInvocation` for any `Customer` argument, and then implement some sort of security logic in the `AuthorizationManager` to ensure the principal is permitted to operate on that customer.
Implementations are expected to return a positive `AuthorizationDecision` if access is granted, negative `AuthorizationDecision` if access is denied, and a null `AuthorizationDecision` when abstaining from making a decision.
`verify` calls `check` and subsequently throws an `AccessDeniedException` in the case of a negative `AuthorizationDecision`.
[[authz-delegate-authorization-manager]]
=== Delegate-based AuthorizationManager Implementations
Whilst users can implement their own `AuthorizationManager` to control all aspects of authorization, Spring Security ships with a delegating `AuthorizationManager` that can collaborate with individual ``AuthorizationManager``s.
`RequestMatcherDelegatingAuthorizationManager` will match the request with the most appropriate delegate `AuthorizationManager`.
For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` and `AuthorizationManagerAfterMethodInterceptor`.
<<authz-authorization-manager-implementations>> illustrates the relevant classes.
[[authz-authorization-manager-implementations]]
.Authorization Manager Implementations
image::{figures}/authorizationhierarchy.png[]
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
[[authz-authority-authorization-manager]]
==== AuthorityAuthorizationManager
The most common `AuthorizationManager` provided with Spring Security is `AuthorityAuthorizationManager`.
It is configured with a given set of authorities to look for on the current `Authentication`.
It will return positive `AuthorizationDecision` should the `Authentication` contain any of the configured authorities.
It will return a negative `AuthorizationDecision` otherwise.
[[authz-authenticated-authorization-manager]]
==== AuthenticatedAuthorizationManager
Another manager is the `AuthenticatedAuthorizationManager`.
It can be used to differentiate between anonymous, fully-authenticated and remember-me authenticated users.
Many sites allow certain limited access under remember-me authentication, but require a user to confirm their identity by logging in for full access.
[[authz-custom-authorization-manager]]
==== Custom Authorization Managers
Obviously, you can also implement a custom `AuthorizationManager` and you can put just about any access-control logic you want in it.
It might be specific to your application (business-logic related) or it might implement some security administration logic.
For example, you can create an implementation that can query Open Policy Agent or your own authorization database.
[TIP]
You'll find a https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time[blog article] on the Spring web site which describes how to use the legacy `AccessDecisionVoter` to deny access in real-time to users whose accounts have been suspended.
You can achieve the same outcome by implementing `AuthorizationManager` instead.
[[authz-voter-adaptation]]
== Adapting AccessDecisionManager and AccessDecisionVoters
Previous to `AuthorizationManager`, Spring Security published <<authz-legacy-note,`AccessDecisionManager` and `AccessDecisionVoter`>>.
In some cases, like migrating an older application, it may be desirable to introduce an `AuthorizationManager` that invokes an `AccessDecisionManager` or `AccessDecisionVoter`.
To call an existing `AccessDecisionManager`, you can do:
.Adapting an AccessDecisionManager
====
.Java
[source,java,role="primary"]
----
@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionManager accessDecisionManager;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
try {
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
return new AuthorizationDecision(true);
} catch (AccessDeniedException ex) {
return new AuthorizationDecision(false);
}
}
@Override
public void verify(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
}
}
----
====
And then wire it into your `SecurityFilterChain`.
Or to only call an `AccessDecisionVoter`, you can do:
.Adapting an AccessDecisionVoter
====
.Java
[source,java,role="primary"]
----
@Component
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionVoter accessDecisionVoter;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
switch (decision) {
case ACCESS_GRANTED:
return new AuthorizationDecision(true);
case ACCESS_DENIED:
return new AuthorizationDecision(false);
}
return null;
}
}
----
====
And then wire it into your `SecurityFilterChain`.
[[authz-hierarchical-roles]]
== Hierarchical Roles
It is a common requirement that a particular role in an application should automatically "include" other roles.
For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can.
To achieve this, you can either make sure that all admin users are also assigned the "user" role.
Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role.
This can get quite complicated if you have a lot of different roles in your application.
The use of a role-hierarchy allows you to configure which roles (or authorities) should include others.
An extended version of Spring Security's `RoleVoter`, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned.
A typical configuration might look like this:
.Hierarchical Roles Configuration
====
.Java
[source,java,role="primary"]
----
@Bean
AccessDecisionVoter hierarchyVoter() {
RoleHierarchy hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST");
return new RoleHierarcyVoter(hierarchy);
}
----
.Xml
[source,java,role="secondary"]
----
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
<constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<property name="hierarchy">
<value>
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
</value>
</property>
</bean>
----
====
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AuthorizationManager` adapted to call the above `RoleHierarchyVoter`.
The `>` symbol can be thought of as meaning "includes".
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user.
For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information.
[[authz-legacy-note]]
== Legacy Authorization Components
[NOTE]
Spring Security contains some legacy components.
Since they are not yet removed, documentation is included for historical purposes.
Their recommended replacements are above.
[[authz-access-decision-manager]]
=== The AccessDecisionManager
@@ -72,8 +266,6 @@ Whilst users can implement their own `AccessDecisionManager` to control all aspe
.Voting Decision Manager
image::{figures}/access-decision-voting.png[]
Using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
The `AccessDecisionManager` then decides whether or not to throw an `AccessDeniedException` based on its assessment of the votes.
@@ -104,7 +296,6 @@ Like the other implementations, there is a parameter that controls the behaviour
It is possible to implement a custom `AccessDecisionManager` that tallies votes differently.
For example, votes from a particular `AccessDecisionVoter` might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect.
[[authz-role-voter]]
==== RoleVoter
The most commonly used `AccessDecisionVoter` provided with Spring Security is the simple `RoleVoter`, which treats configuration attributes as simple role names and votes to grant access if the user has been assigned that role.
@@ -130,14 +321,6 @@ Obviously, you can also implement a custom `AccessDecisionVoter` and you can put
It might be specific to your application (business-logic related) or it might implement some security administration logic.
For example, you'll find a https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time[blog article] on the Spring web site which describes how to use a voter to deny access in real-time to users whose accounts have been suspended.
[[authz-after-invocation-handling]]
== After Invocation Handling
Whilst the `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` before proceeding with the secure object invocation, some applications need a way of modifying the object actually returned by the secure object invocation.
Whilst you could easily implement your own AOP concern to achieve this, Spring Security provides a convenient hook that has several concrete implementations that integrate with its ACL capabilities.
<<authz-after-invocation>> illustrates Spring Security's `AfterInvocationManager` and its concrete implementations.
[[authz-after-invocation]]
.After Invocation Implementation
image::{figures}/after-invocation.png[]
@@ -151,41 +334,3 @@ If you're using the typical Spring Security included `AccessDecisionManager` imp
In turn, if the `AccessDecisionManager` property "`allowIfAllAbstainDecisions`" is `false`, an `AccessDeniedException` will be thrown.
You may avoid this potential issue by either (i) setting "`allowIfAllAbstainDecisions`" to `true` (although this is generally not recommended) or (ii) simply ensure that there is at least one configuration attribute that an `AccessDecisionVoter` will vote to grant access for.
This latter (recommended) approach is usually achieved through a `ROLE_USER` or `ROLE_AUTHENTICATED` configuration attribute.
[[authz-hierarchical-roles]]
== Hierarchical Roles
It is a common requirement that a particular role in an application should automatically "include" other roles.
For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can.
To achieve this, you can either make sure that all admin users are also assigned the "user" role.
Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role.
This can get quite complicated if you have a lot of different roles in your application.
The use of a role-hierarchy allows you to configure which roles (or authorities) should include others.
An extended version of Spring Security's <<authz-role-voter,RoleVoter>>, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned.
A typical configuration might look like this:
[source,xml]
----
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
<constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<property name="hierarchy">
<value>
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
</value>
</property>
</bean>
----
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AccessDecisionManager` configured with the above `RoleHierarchyVoter`.
The `>` symbol can be thought of as meaning "includes".
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user.
For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information.
@@ -0,0 +1,171 @@
[[servlet-authorization-authorizationfilter]]
= Authorize HttpServletRequests with AuthorizationFilter
:figures: servlet/authorization
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet-based applications.
[NOTE]
`AuthorizationFilter` supersedes xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`].
To remain backward compatible, `FilterSecurityInterceptor` remains the default.
This section discusses how `AuthorizationFilter` works and how to override the default configuration.
The {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s.
It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters].
You can override the default when you declare a `SecurityFilterChain`.
Instead of using xref:servlet/authorization/authorize-http-requests.adoc#servlet-authorize-requests-defaults[`authorizeRequests`], use `authorizeHttpRequests`, like so:
.Use authorizeHttpRequests
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated();
)
// ...
return http.build();
}
----
====
This improves on `authorizeRequests` in a number of ways:
1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
This simplifies reuse and customization.
2. Delays `Authentication` lookup.
Instead of the authentication needing to be looked up for every request, it will only look it up in requests where an authorization decision requires authentication.
3. Bean-based configuration support.
When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`].
.Authorize HttpServletRequest
image::{figures}/authorizationfilter.png[]
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
It wraps this in an `Supplier` in order to delay lookup.
* image:{icondir}/number_2.png[] Second, `AuthorizationFilter` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain`.
// FIXME: link to FilterInvocation
* image:{icondir}/number_3.png[] Next, it passes the `Supplier<Authentication>` and `FilterInvocation` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`].
** image:{icondir}/number_4.png[] If authorization is denied, an `AccessDeniedException` is thrown.
In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`.
** image:{icondir}/number_5.png[] If access is granted, `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
We can configure Spring Security to have different rules by adding more rules in order of precedence.
.Authorize Requests
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize // <1>
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2>
.mvcMatchers("/admin/**").hasRole("ADMIN") // <3>
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4>
.anyRequest().denyAll() // <5>
);
return http.build();
}
----
====
<1> There are multiple authorization rules specified.
Each rule is considered in the order they were declared.
<2> We specified multiple URL patterns that any user can access.
Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about".
<3> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN".
You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix.
<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA".
You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix.
<5> Any URL that has not already been matched on is denied access.
This is a good strategy if you do not want to accidentally forget to update your authorization rules.
You can take a bean-based approach by constructing your own xref:servlet/authorization/architecture.adoc#authz-delegate-authorization-manager[`RequestMatcherDelegatingAuthorizationManager`] like so:
.Configure RequestMatcherDelegatingAuthorizationManager
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(access)
)
// ...
return http.build();
}
@Bean
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
RequestMatcher permitAll =
new AndRequestMatcher(
new MvcRequestMatcher(introspector, "/resources/**"),
new MvcRequestMatcher(introspector, "/signup"),
new MvcRequestMatcher(introspector, "/about"));
RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**");
RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**");
RequestMatcher any = AnyRequestMatcher.INSTANCE;
AuthorizationManager<HttpRequestServlet> manager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(permitAll, (context) -> new AuthorizationDecision(true))
.add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
.add(db, AuthorityAuthorizationManager.hasRole("DBA"))
.add(any, new AuthenticatedAuthorizationManager())
.build();
return (context) -> manager.check(context.getRequest());
}
----
====
You can also wire xref:servlet/authorization/architecture.adoc#authz-custom-authorization-manager[your own custom authorization managers] for any request matcher.
Here is an example of mapping a custom authorization manager to the `my/authorized/endpoint`:
.Custom Authorization Manager
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager());
)
// ...
return http.build();
}
----
====
Or you can provide it for all requests as seen below:
.Custom Authorization Manager for All Requests
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest.access(new CustomAuthorizationManager());
)
// ...
return http.build();
}
----
====
@@ -2,6 +2,10 @@
= Authorize HttpServletRequest with FilterSecurityInterceptor
:figures: servlet/authorization
[NOTE]
`FilterSecurityInterceptor` is in the process of being replaced by xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`].
Consider using that instead.
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet based applications.
The {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s.
@@ -14,7 +18,7 @@ image::{figures}/filtersecurityinterceptor.png[]
* image:{icondir}/number_2.png[] Second, `FilterSecurityInterceptor` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain` that are passed into the `FilterSecurityInterceptor`.
// FIXME: link to FilterInvocation
* image:{icondir}/number_3.png[] Next, it passes the `FilterInvocation` to `SecurityMetadataSource` to get the ``ConfigAttribute``s.
* image:{icondir}/number_4.png[] Finally, it passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the `AccessDecisionManager`.
* image:{icondir}/number_4.png[] Finally, it passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the xref:servlet/authorization.adoc#authz-access-decision-manager`AccessDecisionManager`.
** image:{icondir}/number_5.png[] If authorization is denied, an `AccessDeniedException` is thrown.
In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`.
** image:{icondir}/number_6.png[] If access is granted, `FilterSecurityInterceptor` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
@@ -24,6 +28,7 @@ In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilt
By default, Spring Security's authorization will require all requests to be authenticated.
The explicit configuration looks like:
[[servlet-authorize-requests-defaults]]
.Every Request Must be Authenticated
====
.Java
@@ -32,7 +37,7 @@ The explicit configuration looks like:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
@@ -71,7 +76,7 @@ We can configure Spring Security to have different rules by adding more rules in
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize // <1>
.authorizeRequests(authorize -> authorize // <1>
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2>
.mvcMatchers("/admin/**").hasRole("ADMIN") // <3>
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4>
@@ -7,10 +7,11 @@ It is not only useful but necessary to include the user in the queries to suppor
[[data-configuration]]
== Spring Data & Spring Security Configuration
To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`.
In Java Configuration, this would look like:
To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
@@ -18,6 +19,16 @@ public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension {
return SecurityEvaluationContextExtension()
}
----
====
In XML Configuration, this would look like:
[source,xml]
@@ -31,7 +42,9 @@ In XML Configuration, this would look like:
Now Spring Security can be used within your queries.
For example:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Repository
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
@@ -40,6 +53,17 @@ public interface MessageRepository extends PagingAndSortingRepository<Message,Lo
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Repository
interface MessageRepository : PagingAndSortingRepository<Message,Long> {
@Query("select m from Message m where m.to.id = ?#{ principal?.id }")
fun findInbox(pageable: Pageable): Page<Message>
}
----
====
This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`.
Note that this example assumes you have customized the principal to be an Object that has an id property.
By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query.
@@ -323,7 +323,7 @@ Next, we can construct a `JWTProcessor`:
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor();
jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
return jwtProcessor;
}
----
@@ -416,9 +416,9 @@ Now that we have a tenant-aware processor and a tenant-aware validator, we can p
----
@Bean
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
(JwtValidators.createDefault(), this.jwtValidator);
(JwtValidators.createDefault(), jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
@@ -8,7 +8,7 @@ This filter by default responds to endpoint `+/saml2/authenticate/{registrationI
For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
`https://rp.example.org/saml2/authenticate/ping`
`https://rp.example.org/saml2/authenticate/okta`
and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
@@ -187,7 +187,7 @@ But, if you do need something from the request, then you can use create a custom
----
@Component
public class AuthnRequestConverter implements
Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
Converter<Saml2AuthenticationRequestContext, AuthnRequest> {
private final AuthnRequestBuilder authnRequestBuilder;
private final IssuerBuilder issuerBuilder;
@@ -216,12 +216,12 @@ public class AuthnRequestConverter implements
[source,kotlin,role="secondary"]
----
@Component
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
class AuthnRequestConverter : Converter<Saml2AuthenticationRequestContext, AuthnRequest> {
private val authnRequestBuilder: AuthnRequestBuilder? = null
private val issuerBuilder: IssuerBuilder? = null
// ... constructor
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
override fun convert(context: Saml2AuthenticationRequestContext): AuthnRequest {
val myContext: MySaml2AuthenticationRequestContext = context
val issuer: Issuer = issuerBuilder.buildObject()
issuer.value = myContext.getIssuer()
@@ -14,5 +14,5 @@ This process is similar to the one started in 2017 for xref:servlet/oauth2/index
[NOTE]
====
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2/login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
====
@@ -35,6 +35,7 @@ RelyingPartyRegistrationRepository registrations() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("id")
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
.signingX509Credentials((signing) -> signing.add(credential)) <1>
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
@@ -73,6 +74,10 @@ Also, your application can participate in an AP-initiated logout when the assert
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user
4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]
NOTE: Adding `saml2Logout` adds the capability for logout to the service provider.
Because it is an optional capability, you need to enable it for each individual `RelyingPartyRegistration`.
You can do this by setting the `RelyingPartyRegistration.Builder#singleLogoutServiceLocation` property.
== Configuring Logout Endpoints
There are three behaviors that can be triggered by different endpoints:
+1
View File
@@ -1,5 +1,6 @@
plugins {
id "io.github.rwinch.antora" version "0.0.2"
id "org.springframework.antora.check-version"
}
apply plugin: 'io.spring.convention.docs'
+6 -6
View File
@@ -1,11 +1,11 @@
aspectjVersion=1.9.7
springJavaformatVersion=0.0.29
aspectjVersion=1.9.9.1
springJavaformatVersion=0.0.31
springBootVersion=2.4.2
springFrameworkVersion=5.3.13
springFrameworkVersion=5.3.19
openSamlVersion=3.4.6
version=5.6.0
kotlinVersion=1.5.31
samplesBranch=main
version=5.6.3
kotlinVersion=1.5.32
samplesBranch=5.6.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
@@ -10,7 +10,7 @@ dependencies {
implementation 'org.springframework:spring-tx'
testImplementation project(':spring-security-web')
testImplementation 'javax.servlet:javax.servlet-api'
testImplementation 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'org.springframework:spring-web'
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
+2 -2
View File
@@ -5,7 +5,7 @@ dependencies {
implementation 'org.springframework:spring-context'
implementation 'org.springframework:spring-web'
compileOnly 'javax.servlet:javax.servlet-api'
compileOnly 'jakarta.servlet:jakarta.servlet-api'
testImplementation project(':spring-security-core')
testImplementation project(':spring-security-test')
@@ -21,7 +21,7 @@ dependencies {
testImplementation "org.mockito:mockito-core"
testImplementation "org.mockito:mockito-junit-jupiter"
testImplementation "org.springframework:spring-test"
testImplementation 'javax.servlet:javax.servlet-api'
testImplementation 'jakarta.servlet:jakarta.servlet-api'
testRuntimeOnly project(':spring-security-config')
testRuntimeOnly project(':spring-security-ldap')
-1
View File
@@ -25,7 +25,6 @@ dependencies {
}
testImplementation project(':spring-security-test')
testImplementation 'org.slf4j:jcl-over-slf4j'
testImplementation 'org.slf4j:slf4j-api'
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
+1 -2
View File
@@ -12,7 +12,7 @@ dependencies {
optional project(':spring-security-web')
optional 'org.springframework:spring-websocket'
optional 'io.projectreactor:reactor-core'
optional 'javax.servlet:javax.servlet-api'
optional 'jakarta.servlet:jakarta.servlet-api'
testImplementation project(path: ':spring-security-core', configuration: 'tests')
testImplementation 'commons-codec:commons-codec'
@@ -24,7 +24,6 @@ dependencies {
testImplementation "org.mockito:mockito-junit-jupiter"
testImplementation "org.springframework:spring-test"
testImplementation "org.slf4j:slf4j-api"
testImplementation "org.slf4j:jcl-over-slf4j"
testImplementation "org.slf4j:log4j-over-slf4j"
testImplementation "ch.qos.logback:logback-classic"
@@ -23,7 +23,7 @@ dependencies {
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'io.projectreactor.tools:blockhound'
testImplementation 'org.skyscreamer:jsonassert'
testImplementation 'io.r2dbc:r2dbc-h2:0.8.4.RELEASE'
testImplementation 'io.r2dbc:r2dbc-h2:0.8.5.RELEASE'
testImplementation 'io.r2dbc:r2dbc-spi-test:0.8.6.RELEASE'
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
@@ -35,5 +35,5 @@ dependencies {
testRuntimeOnly 'org.hsqldb:hsqldb'
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet:jakarta.servlet-api'
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -204,10 +204,12 @@ public final class OAuth2AuthorizedClientProviderBuilder {
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* token expiry. An access token is considered expired if
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link PasswordGrantBuilder}
* @see PasswordOAuth2AuthorizedClientProvider#setClockSkew(Duration)
*/
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
@@ -275,10 +277,12 @@ public final class OAuth2AuthorizedClientProviderBuilder {
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* token expiry. An access token is considered expired if
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link ClientCredentialsGrantBuilder}
* @see ClientCredentialsOAuth2AuthorizedClientProvider#setClockSkew(Duration)
*/
public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
@@ -365,10 +369,12 @@ public final class OAuth2AuthorizedClientProviderBuilder {
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* token expiry. An access token is considered expired if
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link RefreshTokenGrantBuilder}
* @see RefreshTokenOAuth2AuthorizedClientProvider#setClockSkew(Duration)
*/
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -225,10 +225,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* token expiry. An access token is considered expired if
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link ClientCredentialsGrantBuilder}
* @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration)
*/
public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
@@ -297,10 +299,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* token expiry. An access token is considered expired if
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link PasswordGrantBuilder}
* @see PasswordReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration)
*/
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
@@ -368,10 +372,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* token expiry. An access token is considered expired if
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link RefreshTokenGrantBuilder}
* @see RefreshTokenReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration)
*/
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -54,7 +54,7 @@ public final class JwtClaimValidator<T> implements OAuth2TokenValidator<Jwt> {
Assert.notNull(test, "test can not be null");
this.claim = claim;
this.test = test;
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "The " + this.claim + " claim is not valid",
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The " + this.claim + " claim is not valid",
"https://tools.ietf.org/html/rfc6750#section-3.1");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +16,14 @@
package org.springframework.security.oauth2.jwt;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,7 +49,9 @@ public class JwtClaimValidatorTests {
@Test
public void validateWhenClaimFailsTheTestThenReturnsFailure() {
Jwt jwt = TestJwts.jwt().claim(JwtClaimNames.ISS, "http://abc").build();
Collection<OAuth2Error> details = this.validator.validate(jwt).getErrors();
assertThat(this.validator.validate(jwt).getErrors().isEmpty()).isFalse();
assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN));
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.time.ZoneId;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
@@ -64,6 +65,7 @@ public class JwtTimestampValidatorTests {
.collect(Collectors.toList());
// @formatter:on
assertThat(messages).contains("Jwt expired at " + oneHourAgo);
assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN));
}
@Test
@@ -78,6 +80,7 @@ public class JwtTimestampValidatorTests {
.collect(Collectors.toList());
// @formatter:on
assertThat(messages).contains("Jwt used before " + oneHourFromNow);
assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN));
}
@Test
@@ -12,7 +12,7 @@ dependencies {
optional 'io.projectreactor:reactor-core'
optional 'org.springframework:spring-webflux'
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet:jakarta.servlet-api'
testImplementation project(path: ':spring-security-oauth2-jose', configuration: 'tests')
testImplementation 'com.squareup.okhttp3:mockwebserver'
+7 -2
View File
@@ -10,12 +10,14 @@ dependencies {
api project(':spring-security-web')
api('com.google.inject:guice') {
exclude group: 'aopalliance', module: 'aopalliance'
exclude group: 'javax.inject', module: 'javax.inject'
}
// openid4java has a compile time dep on guice with a group
// name which is different from the maven central one.
// We use the maven central version here instead.
api('org.openid4java:openid4java-nodeps') {
exclude group: 'com.google.code.guice', module: 'guice'
exclude group: 'commons-logging', module: 'commons-logging'
}
api 'org.springframework:spring-aop'
api 'org.springframework:spring-beans'
@@ -23,11 +25,14 @@ dependencies {
api 'org.springframework:spring-core'
api 'org.springframework:spring-web'
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet:jakarta.servlet-api'
runtimeOnly 'net.sourceforge.nekohtml:nekohtml'
runtimeOnly 'org.apache.httpcomponents:httpclient'
runtimeOnly('org.apache.httpcomponents:httpclient') {
exclude group: 'commons-logging', module: 'commons-logging'
}
testImplementation "jakarta.inject:jakarta.inject-api"
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
@@ -50,7 +50,7 @@ dependencies {
opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.0"
opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.0"
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation "org.assertj:assertj-core"
@@ -19,13 +19,12 @@ package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import org.apache.commons.codec.binary.Base64;
import org.springframework.security.saml2.Saml2Exception;
/**
@@ -33,17 +32,15 @@ import org.springframework.security.saml2.Saml2Exception;
*/
final class Saml2Utils {
private static Base64 BASE64 = new Base64(0, new byte[] { '\n' });
private Saml2Utils() {
}
static String samlEncode(byte[] b) {
return BASE64.encodeAsString(b);
return Base64.getMimeEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
return BASE64.decode(s);
return Base64.getMimeDecoder().decode(s);
}
static byte[] samlDeflate(String s) {
@@ -40,11 +40,11 @@ final class Saml2Utils {
}
static String samlEncode(byte[] b) {
return Base64.getEncoder().encodeToString(b);
return Base64.getMimeEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
return Base64.getDecoder().decode(s);
return Base64.getMimeDecoder().decode(s);
}
static byte[] samlDeflate(String s) {
@@ -86,7 +86,9 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
spSsoDescriptor.getKeyDescriptors()
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
if (registration.getSingleLogoutServiceLocation() != null) {
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
}
return spSsoDescriptor;
}
@@ -21,7 +21,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -106,6 +106,8 @@ public final class RelyingPartyRegistration {
Assert.hasText(entityId, "entityId cannot be empty");
Assert.hasText(assertionConsumerServiceLocation, "assertionConsumerServiceLocation cannot be empty");
Assert.notNull(assertionConsumerServiceBinding, "assertionConsumerServiceBinding cannot be null");
Assert.isTrue(singleLogoutServiceLocation == null || singleLogoutServiceBinding != null,
"singleLogoutServiceBinding cannot be null when singleLogoutServiceLocation is set");
Assert.notNull(providerDetails, "providerDetails cannot be null");
Assert.notEmpty(credentials, "credentials cannot be empty");
for (org.springframework.security.saml2.credentials.Saml2X509Credential c : credentials) {
@@ -449,7 +451,7 @@ public final class RelyingPartyRegistration {
org.springframework.security.saml2.credentials.Saml2X509Credential credential) {
PrivateKey privateKey = credential.getPrivateKey();
X509Certificate certificate = credential.getCertificate();
Set<Saml2X509Credential.Saml2X509CredentialType> credentialTypes = new HashSet<>();
Set<Saml2X509Credential.Saml2X509CredentialType> credentialTypes = new LinkedHashSet<>();
if (credential.isSigningCredential()) {
credentialTypes.add(Saml2X509Credential.Saml2X509CredentialType.SIGNING);
}
@@ -469,7 +471,7 @@ public final class RelyingPartyRegistration {
Saml2X509Credential credential) {
PrivateKey privateKey = credential.getPrivateKey();
X509Certificate certificate = credential.getCertificate();
Set<org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType> credentialTypes = new HashSet<>();
Set<org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType> credentialTypes = new LinkedHashSet<>();
if (credential.isSigningCredential()) {
credentialTypes.add(
org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING);
@@ -694,9 +696,9 @@ public final class RelyingPartyRegistration {
private List<String> signingAlgorithms = new ArrayList<>();
private Collection<Saml2X509Credential> verificationX509Credentials = new HashSet<>();
private Collection<Saml2X509Credential> verificationX509Credentials = new LinkedHashSet<>();
private Collection<Saml2X509Credential> encryptionX509Credentials = new HashSet<>();
private Collection<Saml2X509Credential> encryptionX509Credentials = new LinkedHashSet<>();
private String singleSignOnServiceLocation;
@@ -1004,15 +1006,15 @@ public final class RelyingPartyRegistration {
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
private Collection<Saml2X509Credential> signingX509Credentials = new HashSet<>();
private Collection<Saml2X509Credential> signingX509Credentials = new LinkedHashSet<>();
private Collection<Saml2X509Credential> decryptionX509Credentials = new HashSet<>();
private Collection<Saml2X509Credential> decryptionX509Credentials = new LinkedHashSet<>();
private String assertionConsumerServiceLocation = "{baseUrl}/login/saml2/sso/{registrationId}";
private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST;
private String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
private String singleLogoutServiceLocation;
private String singleLogoutServiceResponseLocation;
@@ -1020,7 +1022,7 @@ public final class RelyingPartyRegistration {
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new HashSet<>();
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new LinkedHashSet<>();
private Builder(String registrationId) {
this.registrationId = registrationId;
@@ -111,6 +111,9 @@ final class OpenSamlLogoutRequestResolver {
if (registration == null) {
return null;
}
if (registration.getAssertingPartyDetails().getSingleLogoutServiceLocation() == null) {
return null;
}
LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject();
logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
Issuer issuer = this.issuerBuilder.buildObject();
@@ -132,6 +132,9 @@ final class OpenSamlLogoutResponseResolver {
if (registration == null) {
return null;
}
if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) {
return null;
}
String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST);
byte[] b = Saml2Utils.samlDecode(serialized);
LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b));
@@ -120,6 +120,12 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (registration.getSingleLogoutServiceLocation() == null) {
this.logger.trace(
"Did not process logout request since RelyingPartyRegistration has not been configured with a logout request endpoint");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
if (!isCorrectBinding(request, registration)) {
this.logger.trace("Did not process logout request since used incorrect binding");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
@@ -120,6 +120,12 @@ public final class Saml2LogoutResponseFilter extends OncePerRequestFilter {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString());
return;
}
if (registration.getSingleLogoutServiceResponseLocation() == null) {
this.logger.trace(
"Did not process logout response since RelyingPartyRegistration has not been configured with a logout response endpoint");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
if (!isCorrectBinding(request, registration)) {
this.logger.trace("Did not process logout request since used incorrect binding");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
@@ -40,11 +40,11 @@ final class Saml2Utils {
}
static String samlEncode(byte[] b) {
return Base64.getEncoder().encodeToString(b);
return Base64.getMimeEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
return Base64.getDecoder().decode(s);
return Base64.getMimeDecoder().decode(s);
}
static byte[] samlDeflate(String s) {
@@ -47,7 +47,9 @@ public class OpenSaml3LogoutRequestResolverTests {
this.relyingPartyRegistrationResolver);
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
HttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout"))
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
@@ -53,7 +53,10 @@ public class OpenSaml3LogoutResponseResolverTests {
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
logoutResponseResolver.setParametersConsumer(parametersConsumer);
MockHttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyDetails(
(party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout"))
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter(Saml2ParameterNames.SAML_REQUEST,
@@ -47,7 +47,9 @@ public class OpenSaml4LogoutRequestResolverTests {
this.relyingPartyRegistrationResolver);
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
HttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout"))
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
@@ -53,7 +53,10 @@ public class OpenSaml4LogoutResponseResolverTests {
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
logoutResponseResolver.setParametersConsumer(parametersConsumer);
MockHttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyDetails(
(party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout"))
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter(Saml2ParameterNames.SAML_REQUEST,
@@ -19,28 +19,30 @@ package org.springframework.security.saml2.core;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import org.apache.commons.codec.binary.Base64;
import org.springframework.security.saml2.Saml2Exception;
public final class Saml2Utils {
private static Base64 BASE64 = new Base64(0, new byte[] { '\n' });
private Saml2Utils() {
}
@Deprecated
public static String samlEncodeNotRfc2045(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
public static String samlEncode(byte[] b) {
return BASE64.encodeAsString(b);
return Base64.getMimeEncoder().encodeToString(b);
}
public static byte[] samlDecode(String s) {
return BASE64.decode(s);
return Base64.getMimeDecoder().decode(s);
}
public static byte[] samlDeflate(String s) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,6 +51,10 @@ public final class TestSaml2X509Credentials {
return new Saml2X509Credential(idpCertificate(), Saml2X509CredentialType.VERIFICATION);
}
public static Saml2X509Credential relyingPartyEncryptingCredential() {
return new Saml2X509Credential(idpCertificate(), Saml2X509CredentialType.ENCRYPTION);
}
public static Saml2X509Credential relyingPartySigningCredential() {
return new Saml2X509Credential(spPrivateKey(), spCertificate(), Saml2X509CredentialType.SIGNING);
}
@@ -59,6 +63,16 @@ public final class TestSaml2X509Credentials {
return new Saml2X509Credential(spPrivateKey(), spCertificate(), Saml2X509CredentialType.DECRYPTION);
}
public static Saml2X509Credential altPublicCredential() {
return new Saml2X509Credential(altCertificate(), Saml2X509CredentialType.VERIFICATION,
Saml2X509CredentialType.ENCRYPTION);
}
public static Saml2X509Credential altPrivateCredential() {
return new Saml2X509Credential(altPrivateKey(), altCertificate(), Saml2X509CredentialType.SIGNING,
Saml2X509CredentialType.DECRYPTION);
}
private static X509Certificate certificate(String cert) {
ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes());
try {
@@ -170,4 +184,40 @@ public final class TestSaml2X509Credentials {
+ "-----END PRIVATE KEY-----");
}
private static X509Certificate altCertificate() {
return certificate(
"-----BEGIN CERTIFICATE-----\n" + "MIICkDCCAfkCFEstVfmWSFQp/j88GaMUwqVK72adMA0GCSqGSIb3DQEBCwUAMIGG\n"
+ "MQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjESMBAGA1UEBwwJVmFu\n"
+ "Y291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FNTDEMMAoGA1UECwwD\n"
+ "YWx0MSEwHwYDVQQDDBhhbHQuc3ByaW5nLnNlY3VyaXR5LnNhbWwwHhcNMjIwMjEw\n"
+ "MTY1ODA4WhcNMzIwMjEwMTY1ODA4WjCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgM\n"
+ "Cldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsGA1UECgwUU3ByaW5n\n"
+ "IFNlY3VyaXR5IFNBTUwxDDAKBgNVBAsMA2FsdDEhMB8GA1UEAwwYYWx0LnNwcmlu\n"
+ "Zy5zZWN1cml0eS5zYW1sMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9ZGWj\n"
+ "TPDsymQCJL044py4xLsBI/S9RvzNeR9oD/tHyoxCE+YZzjf0PyBtwqKzkKWqCPf4\n"
+ "XGUYHfEpkM5kJYwCW8TsOx5fnwLIQweiPqjYrBr/O0IjHMqYG9HlR/ros7iBt4ab\n"
+ "EGUu/B9yYg1YRYPxKQ6TNP3AD+9tBT8TsFFyjwIDAQABMA0GCSqGSIb3DQEBCwUA\n"
+ "A4GBAKJf2VHLjkCHRxlbWn63jGiquq3ENYgd1JS0DZ3ggFmuc6zQiqxzRGtArIDZ\n"
+ "0jH5nrG0jcvO0fqDqBQh0iT8thfUnkViAQvACZ9a+0x0NzUicJ+Ra51c8Z2enqbg\n"
+ "pXy+ga67HcAXrDekm1MCGCgiEb/Cgl41lsideqhC8Efl7PRN\n" + "-----END CERTIFICATE-----");
}
private static PrivateKey altPrivateKey() {
return privateKey(
"-----BEGIN PRIVATE KEY-----\n" + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAL1kZaNM8OzKZAIk\n"
+ "vTjinLjEuwEj9L1G/M15H2gP+0fKjEIT5hnON/Q/IG3CorOQpaoI9/hcZRgd8SmQ\n"
+ "zmQljAJbxOw7Hl+fAshDB6I+qNisGv87QiMcypgb0eVH+uizuIG3hpsQZS78H3Ji\n"
+ "DVhFg/EpDpM0/cAP720FPxOwUXKPAgMBAAECgYEApYKslAZ0cer5dSoYNzNLFOnQ\n"
+ "J1H92r/Dw+k6+h0lUvr+keyD5T9jhM76DxHOUDBzpmIKGoDcVDQugk2rILfzXsQA\n"
+ "JtwvDRJk32Z02Vt0jb7t/WUOOQhjKCjQuv9/tOx90GCl0VxYG69UOjaMRWrlg/i9\n"
+ "6/zcTRIahIn5XxF0psECQQD7ivJCpDbOLJGsc8gNJR4cvjZ1q0mHIOrbKqJC0y1n\n"
+ "5DrzGEflPeyCUwnOKNp9HJQP8gmZzXfj0JM9KsjpiUChAkEAwL+FmhDoTiqStIrH\n"
+ "h9Kdnsev//imMmRHxjwDhntYvqavUsISRmY3imd8inoYq5dzWQMzBtoTyMRmqeLT\n"
+ "DHV1LwJAW4xaV37Eo4z9B7Kr4Hzd1MA1ueW5QQDt+Q4vN/r7z4/1FHyFzh0Xcucd\n"
+ "7nZX7qj0CkmgzOVG+Rb0P5LOxJA7gQJBAK1KQ2qNct375qPM9bEGSVGchH6k5X7+\n"
+ "q4ztHdpFgTb/EzdbZiTG935GpjC1rwJuinTnrHOnkwv4j7iDRm24GF8CQQDqPvrQ\n"
+ "GcItR6UUy0q/B8UxLzlE6t+HiznfiJKfyGgCHU56Y4/ZhzSQz2MZHz9SK4DsUL9s\n" + "bOYrWq8VY2fyjV1t\n"
+ "-----END PRIVATE KEY-----");
}
}
@@ -129,6 +129,23 @@ public class OpenSamlLogoutRequestValidatorTests {
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
// gh-10923
@Test
public void handleWhenLogoutResponseHasLineBreaksThenHandles() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
String encoded = new StringBuffer(
Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8))).insert(10, "\r\n")
.toString();
Saml2LogoutRequest request = Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest(encoded)
.build();
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
@@ -119,6 +119,24 @@ public class OpenSamlLogoutResponseValidatorTests {
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE);
}
// gh-10923
@Test
public void handleWhenLogoutResponseHasLineBreaksThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
String encoded = new StringBuilder(
Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8))).insert(10, "\r\n")
.toString();
Saml2LogoutResponse response = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(encoded).build();
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
@@ -61,4 +61,13 @@ public class OpenSamlMetadataResolverTests {
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() {
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
.singleLogoutServiceLocation(null).build();
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
assertThat(metadata).doesNotContain("ResponseLocation");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,8 @@ package org.springframework.security.saml2.provider.service.registration;
import org.junit.jupiter.api.Test;
import org.springframework.security.saml2.credentials.TestSaml2X509Credentials;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import static org.assertj.core.api.Assertions.assertThat;
@@ -81,9 +82,68 @@ public class RelyingPartyRegistrationTests {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("id")
.entityId("entity-id").assertionConsumerServiceLocation("location")
.assertingPartyDetails((assertingParty) -> assertingParty.entityId("entity-id")
.singleSignOnServiceLocation("location"))
.credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())).build();
.singleSignOnServiceLocation("location").verificationX509Credentials(
(c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
.build();
assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
}
@Test
public void buildPreservesCredentialsOrder() {
Saml2X509Credential altRpCredential = TestSaml2X509Credentials.altPrivateCredential();
Saml2X509Credential altApCredential = TestSaml2X509Credentials.altPublicCredential();
Saml2X509Credential verifyingCredential = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
Saml2X509Credential encryptingCredential = TestSaml2X509Credentials.relyingPartyEncryptingCredential();
Saml2X509Credential signingCredential = TestSaml2X509Credentials.relyingPartySigningCredential();
Saml2X509Credential decryptionCredential = TestSaml2X509Credentials.relyingPartyDecryptingCredential();
// Test with the alt credentials first
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyDetails((assertingParty) -> assertingParty.verificationX509Credentials((c) -> {
c.add(altApCredential);
c.add(verifyingCredential);
}).encryptionX509Credentials((c) -> {
c.add(altApCredential);
c.add(encryptingCredential);
})).signingX509Credentials((c) -> {
c.add(altRpCredential);
c.add(signingCredential);
}).decryptionX509Credentials((c) -> {
c.add(altRpCredential);
c.add(decryptionCredential);
}).build();
assertThat(relyingPartyRegistration.getSigningX509Credentials()).containsExactly(altRpCredential,
signingCredential);
assertThat(relyingPartyRegistration.getDecryptionX509Credentials()).containsExactly(altRpCredential,
decryptionCredential);
assertThat(relyingPartyRegistration.getAssertingPartyDetails().getVerificationX509Credentials())
.containsExactly(altApCredential, verifyingCredential);
assertThat(relyingPartyRegistration.getAssertingPartyDetails().getEncryptionX509Credentials())
.containsExactly(altApCredential, encryptingCredential);
// Test with the alt credentials last
relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyDetails((assertingParty) -> assertingParty.verificationX509Credentials((c) -> {
c.add(verifyingCredential);
c.add(altApCredential);
}).encryptionX509Credentials((c) -> {
c.add(encryptingCredential);
c.add(altApCredential);
})).signingX509Credentials((c) -> {
c.add(signingCredential);
c.add(altRpCredential);
}).decryptionX509Credentials((c) -> {
c.add(decryptionCredential);
c.add(altRpCredential);
}).build();
assertThat(relyingPartyRegistration.getSigningX509Credentials()).containsExactly(signingCredential,
altRpCredential);
assertThat(relyingPartyRegistration.getDecryptionX509Credentials()).containsExactly(decryptionCredential,
altRpCredential);
assertThat(relyingPartyRegistration.getAssertingPartyDetails().getVerificationX509Credentials())
.containsExactly(verifyingCredential, altApCredential);
assertThat(relyingPartyRegistration.getAssertingPartyDetails().getEncryptionX509Credentials())
.containsExactly(encryptingCredential, altApCredential);
}
}
@@ -64,7 +64,7 @@ public class Saml2AuthenticationTokenConverterTests {
.willReturn(this.relyingPartyRegistration);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.SAML_RESPONSE,
Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8)));
Saml2Utils.samlEncodeNotRfc2045("response".getBytes(StandardCharsets.UTF_8)));
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
assertThat(token.getRelyingPartyRegistration().getRegistrationId())
@@ -115,7 +115,7 @@ public class Saml2AuthenticationTokenConverterTests {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
byte[] deflated = Saml2Utils.samlDeflate("response");
String encoded = Saml2Utils.samlEncode(deflated);
String encoded = Saml2Utils.samlEncodeNotRfc2045(deflated);
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded);
Saml2AuthenticationToken token = converter.convert(request);
assertThat(token.getSaml2Response()).isEqualTo("response");
@@ -98,6 +98,29 @@ public class OpenSamlLogoutResponseResolverTests {
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
}
// gh-10923
@Test
public void resolvePostWithLineBreaksWhenAuthenticatedThenSuccess() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
MockHttpServletRequest request = new MockHttpServletRequest();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
String encoded = new StringBuffer(
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes())).insert(10, "\r\n")
.toString();
request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded);
request.setParameter(Saml2ParameterNames.RELAY_STATE, "abcd");
Authentication authentication = authentication(registration);
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse saml2LogoutResponse = this.logoutResponseResolver.resolve(request, authentication);
assertThat(saml2LogoutResponse.getParameter(Saml2ParameterNames.SIG_ALG)).isNull();
assertThat(saml2LogoutResponse.getParameter(Saml2ParameterNames.SIGNATURE)).isNull();
assertThat(saml2LogoutResponse.getParameter(Saml2ParameterNames.RELAY_STATE)).isSameAs("abcd");
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding);
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
@@ -153,4 +153,20 @@ public class Saml2LogoutRequestFilterTests {
verifyNoInteractions(this.logoutHandler);
}
@Test
public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
MockHttpServletResponse response = new MockHttpServletResponse();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().singleLogoutServiceLocation(null)
.build();
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
assertThat(response.getStatus()).isEqualTo(401);
verifyNoInteractions(this.logoutHandler);
}
}
@@ -37,6 +37,7 @@ import org.springframework.security.saml2.provider.service.registration.TestRely
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
@@ -151,4 +152,23 @@ public class Saml2LogoutResponseFilterTests {
verifyNoInteractions(this.logoutSuccessHandler);
}
@Test
public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "response");
MockHttpServletResponse response = new MockHttpServletResponse();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().singleLogoutServiceLocation(null)
.singleLogoutServiceResponseLocation(null).build();
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest("request").build();
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
assertThat(response.getStatus()).isEqualTo(401);
verifyNoInteractions(this.logoutSuccessHandler);
}
}
-2
View File
@@ -10,8 +10,6 @@ plugins {
id "io.spring.ge.conventions" version "0.0.7"
}
enableFeaturePreview("VERSION_ORDERING_V2")
dependencyResolutionManagement {
repositories {
mavenCentral()
+3 -3
View File
@@ -12,10 +12,10 @@ dependencies {
api 'org.springframework:spring-expression'
api 'org.springframework:spring-web'
provided 'javax.servlet.jsp:javax.servlet.jsp-api'
provided 'javax.servlet:javax.servlet-api'
provided 'jakarta.servlet.jsp:jakarta.servlet.jsp-api'
provided 'jakarta.servlet:jakarta.servlet-api'
testRuntimeOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api'
testRuntimeOnly 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"

Some files were not shown because too many files have changed in this diff Show More