diff --git a/.github/actions/algolia-config.json b/.github/actions/algolia-config.json new file mode 100644 index 00000000000..09d30d486ea --- /dev/null +++ b/.github/actions/algolia-config.json @@ -0,0 +1,20 @@ +{ + "index_name": "security-docs", + "start_urls": [ + "https://docs.spring.io/spring-security/reference/" + ], + "selectors": { + "lvl0": { + "selector": "//nav[@class='crumbs']//li[@class='crumb'][last()-1]", + "type": "xpath", + "global": true, + "default_value": "Home" + }, + "lvl1": ".doc h1", + "lvl2": ".doc h2", + "lvl3": ".doc h3", + "lvl4": ".doc h4", + "text": ".doc p, .doc td.content, .doc th.tableblock" + } +} + diff --git a/.github/actions/algolia-deploy.sh b/.github/actions/algolia-deploy.sh new file mode 100755 index 00000000000..994dfee9ac9 --- /dev/null +++ b/.github/actions/algolia-deploy.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +HOST="$1" +HOST_PATH="$2" +SSH_PRIVATE_KEY="$3" +SSH_KNOWN_HOST="$4" + + +if [ "$#" -ne 4 ]; then + echo -e "not enough arguments USAGE:\n\n$0 \$HOST \$HOST_PATH \$SSH_PRIVATE_KEY \$SSH_KNOWN_HOSTS \n\n" >&2 + exit 1 +fi + +# Use a non-default path to avoid overriding when testing locally +SSH_PRIVATE_KEY_PATH=~/.ssh/github-actions-docs +install -m 600 -D /dev/null "$SSH_PRIVATE_KEY_PATH" +echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_PATH" +echo "$SSH_KNOWN_HOST" > ~/.ssh/known_hosts +rsync --delete -avze "ssh -i $SSH_PRIVATE_KEY_PATH" docs/build/site/ "$HOST:$HOST_PATH" +rm -f "$SSH_PRIVATE_KEY_PATH" diff --git a/.github/actions/algolia-docsearch-scraper.sh b/.github/actions/algolia-docsearch-scraper.sh new file mode 100755 index 00000000000..2bb9ce178ac --- /dev/null +++ b/.github/actions/algolia-docsearch-scraper.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +### +# Docs +# config.json https://docsearch.algolia.com/docs/config-file +# Run the crawler https://docsearch.algolia.com/docs/run-your-own/#run-the-crawl-from-the-docker-image + +### USAGE +if [ "$#" -ne 3 ]; then + echo -e "not enough arguments USAGE:\n\n$0 \$ALGOLIA_APPLICATION_ID \$ALGOLIA_API_KEY \$CONFIG_FILE\n\n" >&2 + exit 1 +fi + +# Script Parameters +APPLICATION_ID=$1 +API_KEY=$2 +CONFIG_FILE=$3 + +#### Script +script_dir=$(dirname $0) +docker run -e "APPLICATION_ID=$APPLICATION_ID" -e "API_KEY=$API_KEY" -e "CONFIG=$(cat $CONFIG_FILE | jq -r tostring)" algolia/docsearch-scraper diff --git a/.github/actions/dispatch.sh b/.github/actions/dispatch.sh index d6c2a37794e..955e9cbbeef 100755 --- a/.github/actions/dispatch.sh +++ b/.github/actions/dispatch.sh @@ -1,5 +1,5 @@ REPOSITORY_REF="$1" TOKEN="$2" -curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches -echo "Requested Build for $REPOSITORY_REF" \ No newline at end of file +curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build-reference"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches +echo "Requested Build for $REPOSITORY_REF" diff --git a/.github/workflows/algolia-index.yml b/.github/workflows/algolia-index.yml new file mode 100644 index 00000000000..dfc2295af33 --- /dev/null +++ b/.github/workflows/algolia-index.yml @@ -0,0 +1,16 @@ +name: Update Algolia Index + +on: + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: # Manual trigger + +jobs: + update: + name: Update Algolia Index + runs-on: ubuntu-latest + steps: + - name: Checkout Source + uses: actions/checkout@v2 + - name: Update Index + run: ${GITHUB_WORKSPACE}/.github/actions/algolia-docsearch-scraper.sh "${{ secrets.ALGOLIA_APPLICATION_ID }}" "${{ secrets.ALGOLIA_WRITE_API_KEY }}" "${GITHUB_WORKSPACE}/.github/actions/algolia-config.json" diff --git a/.github/workflows/build-reference.yml b/.github/workflows/antora-generate.yml similarity index 88% rename from .github/workflows/build-reference.yml rename to .github/workflows/antora-generate.yml index 7387f4a0407..089f0ac041d 100644 --- a/.github/workflows/build-reference.yml +++ b/.github/workflows/antora-generate.yml @@ -1,9 +1,11 @@ -name: reference +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 }} @@ -27,4 +29,4 @@ jobs: repository-name: "spring-io/spring-generated-docs" token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - name: Dispatch Build Request - run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-io/spring-reference' "$GH_ACTIONS_REPO_TOKEN" + run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-projects/spring-security' "$GH_ACTIONS_REPO_TOKEN" diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml new file mode 100644 index 00000000000..c9649439360 --- /dev/null +++ b/.github/workflows/backport-bot.yml @@ -0,0 +1,26 @@ +name: Backport Bot + +on: + issues: + types: [labeled] + pull_request: + types: [labeled] + push: + branches: + - '*.x' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Download BackportBot + run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar + - name: Backport + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT: ${{ toJSON(github.event) }} + run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT" diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index d3679cc30fa..422b58c4f32 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -95,11 +95,15 @@ jobs: mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - name: Check samples project + env: + LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos + SAMPLES_INIT_SCRIPT: ${{ github.workspace }}/build/includeRepo/spring-security-ci.gradle run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" - ./gradlew checkSamples --stacktrace + ./gradlew publishMavenJavaPublicationToLocalRepository + ./gradlew checkSamples -PsamplesInitScript="$SAMPLES_INIT_SCRIPT" -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" --stacktrace check_tangles: name: Check for Package Tangles needs: [ prerequisites ] diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml new file mode 100644 index 00000000000..a0033b926b0 --- /dev/null +++ b/.github/workflows/deploy-reference.yml @@ -0,0 +1,33 @@ +name: Build & Deploy Reference + +on: + repository_dispatch: + types: request-build-reference + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: # Manual trigger + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + cache: gradle + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Build with Gradle + run: ./gradlew :spring-security-docs:antora --stacktrace + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties + - name: Deploy + run: ${GITHUB_WORKSPACE}/.github/actions/algolia-deploy.sh "${{ secrets.DOCS_USERNAME }}@${{ secrets.DOCS_HOST }}" "/opt/www/domains/spring.io/docs/htdocs/spring-security/reference/" "${{ secrets.DOCS_SSH_KEY }}" "${{ secrets.DOCS_SSH_HOST_KEY }}" diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 00000000000..6da1a6e2097 --- /dev/null +++ b/.sdkmanrc @@ -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 diff --git a/RELEASE.adoc b/RELEASE.adoc index 964c7d48e78..b86aa832ced 100644 --- a/RELEASE.adoc +++ b/RELEASE.adoc @@ -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 diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java index 9d4d099b257..e49fdb58765 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java @@ -42,7 +42,7 @@ import org.springframework.security.acls.domain.DefaultPermissionFactory; import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy; import org.springframework.security.acls.domain.GrantedAuthoritySid; -import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.ObjectIdentityRetrievalStrategyImpl; import org.springframework.security.acls.domain.PermissionFactory; import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.acls.model.AccessControlEntry; @@ -51,6 +51,7 @@ import org.springframework.security.acls.model.MutableAcl; import org.springframework.security.acls.model.NotFoundException; import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.ObjectIdentityGenerator; import org.springframework.security.acls.model.Permission; import org.springframework.security.acls.model.PermissionGrantingStrategy; import org.springframework.security.acls.model.Sid; @@ -73,8 +74,8 @@ * one in lookupObjectIdentities. These are built from the same select and "order * by" clause, using a different where clause in each case. In order to use custom schema * or column names, each of these SQL clauses can be customized, but they must be - * consistent with each other and with the expected result set generated by the the - * default values. + * consistent with each other and with the expected result set generated by the default + * values. * * @author Ben Alex */ @@ -109,6 +110,8 @@ public class BasicLookupStrategy implements LookupStrategy { private final AclAuthorizationStrategy aclAuthorizationStrategy; + private ObjectIdentityGenerator objectIdentityGenerator; + private PermissionFactory permissionFactory = new DefaultPermissionFactory(); private final AclCache aclCache; @@ -162,6 +165,7 @@ public BasicLookupStrategy(DataSource dataSource, AclCache aclCache, this.aclCache = aclCache; this.aclAuthorizationStrategy = aclAuthorizationStrategy; this.grantingStrategy = grantingStrategy; + this.objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl(); this.aclClassIdUtils = new AclClassIdUtils(); this.fieldAces.setAccessible(true); this.fieldAcl.setAccessible(true); @@ -488,6 +492,11 @@ public final void setAclClassIdSupported(boolean aclClassIdSupported) { } } + public final void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { + Assert.notNull(objectIdentityGenerator, "objectIdentityGenerator cannot be null"); + this.objectIdentityGenerator = objectIdentityGenerator; + } + public final void setConversionService(ConversionService conversionService) { this.aclClassIdUtils = new AclClassIdUtils(conversionService); } @@ -569,7 +578,8 @@ private void convertCurrentResultIntoObject(Map acls, ResultS // target id type, e.g. UUID. Serializable identifier = (Serializable) rs.getObject("object_id_identity"); identifier = BasicLookupStrategy.this.aclClassIdUtils.identifierFrom(identifier, rs); - ObjectIdentity objectIdentity = new ObjectIdentityImpl(rs.getString("class"), identifier); + ObjectIdentity objectIdentity = BasicLookupStrategy.this.objectIdentityGenerator + .createObjectIdentity(identifier, rs.getString("class")); Acl parentAcl = null; long parentAclId = rs.getLong("parent_object"); diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java b/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java index 935466f5d1d..e499577388c 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java @@ -31,11 +31,12 @@ import org.springframework.core.convert.ConversionService; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.ObjectIdentityRetrievalStrategyImpl; import org.springframework.security.acls.model.Acl; import org.springframework.security.acls.model.AclService; import org.springframework.security.acls.model.NotFoundException; import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.ObjectIdentityGenerator; import org.springframework.security.acls.model.Sid; import org.springframework.util.Assert; @@ -81,6 +82,8 @@ public class JdbcAclService implements AclService { private AclClassIdUtils aclClassIdUtils; + private ObjectIdentityGenerator objectIdentityGenerator; + public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) { this(new JdbcTemplate(dataSource), lookupStrategy); } @@ -91,6 +94,7 @@ public JdbcAclService(JdbcOperations jdbcOperations, LookupStrategy lookupStrate this.jdbcOperations = jdbcOperations; this.lookupStrategy = lookupStrategy; this.aclClassIdUtils = new AclClassIdUtils(); + this.objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl(); } @Override @@ -105,7 +109,7 @@ private ObjectIdentity mapObjectIdentityRow(ResultSet rs) throws SQLException { String javaType = rs.getString("class"); Serializable identifier = (Serializable) rs.getObject("obj_id"); identifier = this.aclClassIdUtils.identifierFrom(identifier, rs); - return new ObjectIdentityImpl(javaType, identifier); + return this.objectIdentityGenerator.createObjectIdentity(identifier, javaType); } @Override @@ -165,6 +169,11 @@ public void setConversionService(ConversionService conversionService) { this.aclClassIdUtils = new AclClassIdUtils(conversionService); } + public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { + Assert.notNull(objectIdentityGenerator, "objectIdentityGenerator cannot be null"); + this.objectIdentityGenerator = objectIdentityGenerator; + } + protected boolean isAclClassIdSupported() { return this.aclClassIdSupported; } diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java index 4a6b1d695f6..e1be44924db 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java @@ -318,4 +318,13 @@ public void testCreateGrantedAuthority() { assertThat(((GrantedAuthoritySid) result).getGrantedAuthority()).isEqualTo("sid"); } + @Test + public void setObjectIdentityGeneratorWhenNullThenThrowsIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.strategy.setObjectIdentityGenerator(null)) + .withMessage("objectIdentityGenerator cannot be null"); + // @formatter:on + } + } diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java index 903fd3264ce..cd91ae17453 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java @@ -45,6 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; @@ -170,6 +171,26 @@ public void findChildrenOfIdTypeUUID() { .isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762")); } + @Test + public void setObjectIdentityGeneratorWhenNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.aclServiceIntegration.setObjectIdentityGenerator(null)) + .withMessage("objectIdentityGenerator cannot be null"); + } + + @Test + public void findChildrenWhenObjectIdentityGeneratorSetThenUsed() { + this.aclServiceIntegration + .setObjectIdentityGenerator((id, type) -> new ObjectIdentityImpl(type, "prefix:" + id)); + + ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US"); + this.aclServiceIntegration.setAclClassIdSupported(true); + List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); + assertThat(objectIdentities.size()).isEqualTo(1); + assertThat(objectIdentities.get(0).getType()).isEqualTo("location"); + assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL"); + } + class MockLongIdDomainObject { private Object id; diff --git a/aspects/spring-security-aspects.gradle b/aspects/spring-security-aspects.gradle index 3a58595619f..d3bbe9cbc0e 100644 --- a/aspects/spring-security-aspects.gradle +++ b/aspects/spring-security-aspects.gradle @@ -27,7 +27,3 @@ sourceSets.test.aspectj.srcDir "src/test/java" sourceSets.test.java.srcDirs = files() compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml" - -aspectj { - version = aspectjVersion -} diff --git a/build.gradle b/build.gradle index 20f17eab4f7..a871cde36a0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,13 +2,12 @@ buildscript { dependencies { classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion" classpath 'io.spring.nohttp:nohttp-gradle:0.0.10' - classpath "io.freefair.gradle:aspectj-plugin:6.2.0" + classpath "io.freefair.gradle:aspectj-plugin:6.4.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.netflix.nebula:nebula-project-plugin:8.2.0" } repositories { - maven { url 'https://repo.spring.io/plugins-snapshot' } - maven { url 'https://plugins.gradle.org/m2/' } + gradlePluginPortal() } } @@ -22,6 +21,7 @@ apply plugin: 'org.springframework.security.update-dependencies' apply plugin: 'org.springframework.security.sagan' apply plugin: 'org.springframework.github.milestone' apply plugin: 'org.springframework.github.changelog' +apply plugin: 'org.springframework.github.release' group = 'org.springframework.security' description = 'Spring Security' @@ -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/" } @@ -46,6 +46,13 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") { } } +tasks.named("createGitHubRelease") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + tasks.named("updateDependencies") { // we aren't Gradle 7 compatible yet checkForGradleUpdate = false @@ -64,16 +71,8 @@ updateDependenciesSettings { dependencyExcludes { majorVersionBump() alphaBetaVersions() - releaseCandidatesVersions() - milestoneVersions() snapshotVersions() addRule { components -> - components.withModule("commons-codec:commons-codec") { selection -> - ModuleComponentIdentifier candidate = selection.getCandidate(); - if (!candidate.getVersion().equals(selection.getCurrentVersion())) { - selection.reject("commons-codec updates break saml tests"); - } - } components.withModule("org.python:jython") { selection -> ModuleComponentIdentifier candidate = selection.getCandidate(); if (!candidate.getVersion().equals(selection.getCurrentVersion())) { @@ -133,6 +132,13 @@ allprojects { } } } + + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(8) + } + } + } if (hasProperty('buildScan')) { @@ -152,6 +158,10 @@ tasks.register('checkSamples') { includeCheckRemote { repository = 'spring-projects/spring-security-samples' ref = samplesBranch + if (project.hasProperty("samplesInitScript")) { + initScripts = [samplesInitScript] + projectProperties = ["localRepositoryPath": localRepositoryPath, "springSecurityVersion": project.version] + } } dependsOn checkRemote } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 18001d9e6a3..3a24e0555a7 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -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" @@ -56,6 +59,10 @@ gradlePlugin { id = "org.springframework.github.changelog" implementationClass = "org.springframework.gradle.github.changelog.GitHubChangelogPlugin" } + githubRelease { + id = "org.springframework.github.release" + implementationClass = "org.springframework.gradle.github.release.GitHubReleasePlugin" + } s101 { id = "s101" implementationClass = "s101.S101Plugin" @@ -73,10 +80,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.11' + implementation 'io.projectreactor:reactor-core:3.5.0-M1' 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 +96,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 +108,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' + ) } diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.jar b/buildSrc/gradle/wrapper/gradle-wrapper.jar index e708b1c023e..7454180f2ae 100644 Binary files a/buildSrc/gradle/wrapper/gradle-wrapper.jar and b/buildSrc/gradle/wrapper/gradle-wrapper.jar differ diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.properties b/buildSrc/gradle/wrapper/gradle-wrapper.properties index ffed3a254e9..e750102e092 100644 --- a/buildSrc/gradle/wrapper/gradle-wrapper.properties +++ b/buildSrc/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy index d0a64ab85bb..c62fef79b1c 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy @@ -25,7 +25,7 @@ public class DocsPlugin implements Plugin { 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) { diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy index 5ba6e350f4a..929338cc6f0 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. You may obtain a copy of @@ -19,7 +19,6 @@ package io.spring.gradle.convention import io.spring.gradle.IncludeRepoTask import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.provider.Property import org.gradle.api.tasks.GradleBuild import org.gradle.api.tasks.TaskProvider @@ -40,6 +39,12 @@ class IncludeCheckRemotePlugin implements Plugin { it.dependsOn 'includeRepo' it.dir = includeRepoTask.get().outputDirectory it.tasks = extension.getTasks() + extension.getInitScripts().forEach {script -> + it.startParameter.addInitScript(new File(script)) + } + extension.getProjectProperties().entrySet().forEach { entry -> + it.startParameter.projectProperties.put(entry.getKey(), entry.getValue()) + } } } @@ -60,6 +65,16 @@ class IncludeCheckRemotePlugin implements Plugin { */ List tasks = ['check'] + /** + * Init scripts for the build + */ + List initScripts = [] + + /** + * Map of properties for the build + */ + Map projectProperties = [:] + } } diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy index 9858458b8fe..059fdfbc8b6 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy @@ -59,14 +59,22 @@ public class IntegrationTestPlugin implements Plugin { integrationTestRuntime { extendsFrom integrationTestCompile, testRuntime, testRuntimeOnly } + integrationTestCompileClasspath { + extendsFrom integrationTestCompile + canBeResolved = true + } + integrationTestRuntimeClasspath { + extendsFrom integrationTestRuntime + canBeResolved = true + } } project.sourceSets { integrationTest { java.srcDir project.file('src/integration-test/java') resources.srcDir project.file('src/integration-test/resources') - compileClasspath = project.sourceSets.main.output + project.sourceSets.test.output + project.configurations.integrationTestCompile - runtimeClasspath = output + compileClasspath + project.configurations.integrationTestRuntime + compileClasspath = project.sourceSets.main.output + project.sourceSets.test.output + project.configurations.integrationTestCompileClasspath + runtimeClasspath = output + compileClasspath + project.configurations.integrationTestRuntimeClasspath } } @@ -85,7 +93,7 @@ public class IntegrationTestPlugin implements Plugin { project.idea { module { testSourceDirs += project.file('src/integration-test/java') - scopes.TEST.plus += [ project.configurations.integrationTestCompile ] + scopes.TEST.plus += [ project.configurations.integrationTestCompileClasspath ] } } } @@ -115,7 +123,7 @@ public class IntegrationTestPlugin implements Plugin { project.plugins.withType(EclipsePlugin) { project.eclipse.classpath { - plusConfigurations += [ project.configurations.integrationTestCompile ] + plusConfigurations += [ project.configurations.integrationTestCompileClasspath ] } } } diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy index c2420814290..407163d82a0 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy @@ -35,11 +35,6 @@ class RepositoryConventionPlugin implements Plugin { mavenLocal() } mavenCentral() - jcenter() { - content { - includeGroup "org.gretty" - } - } if (isSnapshot) { maven { name = 'artifactory-snapshot' diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy index f06b15f508e..506c5e077b9 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy @@ -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 { @@ -32,6 +33,7 @@ class RootProjectPlugin implements Plugin { pluginManager.apply(SchemaPlugin) pluginManager.apply(NoHttpPlugin) pluginManager.apply(SpringNexusPublishPlugin) + pluginManager.apply(CheckProhibitedDependenciesLifecyclePlugin) pluginManager.apply("org.sonarqube") project.repositories.mavenCentral() diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy index 36a7013f55b..0c1027f4f44 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy @@ -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") diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java new file mode 100644 index 00000000000..464b7ce677a --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -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 { + public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion"; + + @Override + public void apply(Project project) { + TaskProvider antoraCheckVersion = project.getTasks().register(ANTORA_CHECK_VERSION_TASK_NAME, CheckAntoraVersionTask.class, new Action() { + @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() { + @Override + public void execute(LifecycleBasePlugin lifecycleBasePlugin) { + project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME).configure(new Action() { + @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; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java new file mode 100644 index 00000000000..ae26a92f0f6 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -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 getAntoraVersion(); + + @Input + @Optional + public abstract Property getAntoraPrerelease(); + + @Input + @Optional + public abstract Property 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; + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java new file mode 100644 index 00000000000..4738135e90d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java @@ -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 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; + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java new file mode 100644 index 00000000000..0791a193e89 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java @@ -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 { + + @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 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)); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java new file mode 100644 index 00000000000..77fd369528f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java @@ -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 { + public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; + + @Override + public void apply(Project project) { + TaskProvider 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); + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java similarity index 93% rename from buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java rename to buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java index 70eec3b1505..e570a47e902 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java @@ -1,10 +1,11 @@ -package org.springframework.gradle.github.milestones; +package org.springframework.gradle.github; + public class RepositoryRef { private String owner; private String name; - RepositoryRef() { + public RepositoryRef() { } public RepositoryRef(String owner, String name) { @@ -62,4 +63,3 @@ public RepositoryRef build() { } } } - diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java index 2000e1a378c..0eab3d80068 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java @@ -16,6 +16,9 @@ package org.springframework.gradle.github.changelog; +import java.io.File; +import java.nio.file.Paths; + import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -28,12 +31,10 @@ import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout; import org.gradle.api.tasks.JavaExec; -import java.io.File; -import java.nio.file.Paths; - public class GitHubChangelogPlugin implements Plugin { public static final String CHANGELOG_GENERATOR_CONFIGURATION_NAME = "changelogGenerator"; + public static final String RELEASE_NOTES_PATH = "changelog/release-notes.md"; @Override public void apply(Project project) { @@ -42,7 +43,7 @@ public void apply(Project project) { project.getTasks().register("generateChangelog", JavaExec.class, new Action() { @Override public void execute(JavaExec generateChangelog) { - File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), "changelog/release-notes.md")); + File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), RELEASE_NOTES_PATH)); outputFile.getParentFile().mkdirs(); generateChangelog.setGroup("Release"); generateChangelog.setDescription("Generates the changelog"); diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java index 31f1274adb4..fd3c0d817bb 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java @@ -16,6 +16,9 @@ package org.springframework.gradle.github.milestones; +import java.io.IOException; +import java.util.List; + import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import okhttp3.Interceptor; @@ -23,8 +26,7 @@ import okhttp3.Request; import okhttp3.Response; -import java.io.IOException; -import java.util.List; +import org.springframework.gradle.github.RepositoryRef; public class GitHubMilestoneApi { private String baseUrl = "https://api.github.com"; diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java index de846378f7a..40b026c8045 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java @@ -21,6 +21,8 @@ import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; +import org.springframework.gradle.github.RepositoryRef; + public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { @Input private RepositoryRef repository = new RepositoryRef(); diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java index 527b7676133..81663f25611 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java @@ -29,7 +29,7 @@ public void execute(GitHubMilestoneHasNoOpenIssuesTask githubCheckMilestoneHasNo githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); - if (project.hasProperty("githubAccessToken")) { + if (project.hasProperty("gitHubAccessToken")) { githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java new file mode 100644 index 00000000000..65c8b687be0 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; +import org.springframework.gradle.github.changelog.GitHubChangelogPlugin; + +/** + * @author Steve Riesenberg + */ +public class CreateGitHubReleaseTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input @Optional + private String gitHubAccessToken; + + @Input + private String version; + + @Input @Optional + private String branch = "main"; + + @Input + private boolean createRelease = false; + + @TaskAction + public void createGitHubRelease() { + String body = readReleaseNotes(); + Release release = Release.tag(this.version) + .commit(this.branch) + .name(this.version) + .body(body) + .preRelease(this.version.contains("-")) + .build(); + + System.out.printf("%sCreating GitHub release for %s/%s@%s\n", + this.createRelease ? "" : "[DRY RUN] ", + this.repository.getOwner(), + this.repository.getName(), + this.version + ); + System.out.printf(" Release Notes:\n\n----\n%s\n----\n\n", body.trim()); + + if (this.createRelease) { + GitHubReleaseApi github = new GitHubReleaseApi(this.gitHubAccessToken); + github.publishRelease(this.repository, release); + } + } + + private String readReleaseNotes() { + Project project = getProject(); + File inputFile = project.file(Paths.get(project.getBuildDir().getPath(), GitHubChangelogPlugin.RELEASE_NOTES_PATH)); + try { + return Files.readString(inputFile.toPath()); + } catch (IOException ex) { + throw new RuntimeException("Unable to read release notes from " + inputFile, ex); + } + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public boolean isCreateRelease() { + return createRelease; + } + + public void setCreateRelease(boolean createRelease) { + this.createRelease = createRelease; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java new file mode 100644 index 00000000000..65238d0b821 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.io.IOException; + +import com.google.gson.Gson; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * Manage GitHub releases. + * + * @author Steve Riesenberg + */ +public class GitHubReleaseApi { + private String baseUrl = "https://api.github.com"; + + private final OkHttpClient httpClient; + private Gson gson = new Gson(); + + public GitHubReleaseApi(String gitHubAccessToken) { + this.httpClient = new OkHttpClient.Builder() + .addInterceptor(new AuthorizationInterceptor(gitHubAccessToken)) + .build(); + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Publish a release with no binary attachments. + * + * @param repository The repository owner/name + * @param release The contents of the release + */ + public void publishRelease(RepositoryRef repository, Release release) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/releases"; + String json = this.gson.toJson(release); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.httpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s. Got response %s", + release.getName(), repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s", + release.getName(), repository.getOwner(), repository.getName()), ex); + } + } + + private static class AuthorizationInterceptor implements Interceptor { + private final String token; + + public AuthorizationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer " + this.token) + .build(); + + return chain.proceed(request); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java new file mode 100644 index 00000000000..ae2c44a7694 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import groovy.lang.MissingPropertyException; +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** + * @author Steve Riesenberg + */ +public class GitHubReleasePlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, new Action() { + @Override + public void execute(CreateGitHubReleaseTask createGitHubRelease) { + createGitHubRelease.setGroup("Release"); + createGitHubRelease.setDescription("Create a github release"); + createGitHubRelease.dependsOn("generateChangelog"); + + createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); + createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); + if (project.hasProperty("branch")) { + createGitHubRelease.setBranch((String) project.findProperty("branch")); + } + createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { + throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); + } + } + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java new file mode 100644 index 00000000000..6dec2ceb796 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Steve Riesenberg + */ +public class Release { + @SerializedName("tag_name") + private final String tag; + + @SerializedName("target_commitish") + private final String commit; + + @SerializedName("name") + private final String name; + + @SerializedName("body") + private final String body; + + @SerializedName("draft") + private final boolean draft; + + @SerializedName("prerelease") + private final boolean preRelease; + + @SerializedName("generate_release_notes") + private final boolean generateReleaseNotes; + + private Release(String tag, String commit, String name, String body, boolean draft, boolean preRelease, boolean generateReleaseNotes) { + this.tag = tag; + this.commit = commit; + this.name = name; + this.body = body; + this.draft = draft; + this.preRelease = preRelease; + this.generateReleaseNotes = generateReleaseNotes; + } + + public String getTag() { + return tag; + } + + public String getCommit() { + return commit; + } + + public String getName() { + return name; + } + + public String getBody() { + return body; + } + + public boolean isDraft() { + return draft; + } + + public boolean isPreRelease() { + return preRelease; + } + + public boolean isGenerateReleaseNotes() { + return generateReleaseNotes; + } + + @Override + public String toString() { + return "Release{" + + "tag='" + tag + '\'' + + ", commit='" + commit + '\'' + + ", name='" + name + '\'' + + ", body='" + body + '\'' + + ", draft=" + draft + + ", preRelease=" + preRelease + + ", generateReleaseNotes=" + generateReleaseNotes + + '}'; + } + + public static Builder tag(String tag) { + return new Builder().tag(tag); + } + + public static Builder commit(String commit) { + return new Builder().commit(commit); + } + + public static final class Builder { + private String tag; + private String commit; + private String name; + private String body; + private boolean draft; + private boolean preRelease; + private boolean generateReleaseNotes; + + private Builder() { + } + + public Builder tag(String tag) { + this.tag = tag; + return this; + } + + public Builder commit(String commit) { + this.commit = commit; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Builder draft(boolean draft) { + this.draft = draft; + return this; + } + + public Builder preRelease(boolean preRelease) { + this.preRelease = preRelease; + return this; + } + + public Builder generateReleaseNotes(boolean generateReleaseNotes) { + this.generateReleaseNotes = generateReleaseNotes; + return this; + } + + public Release build() { + return new Release(tag, commit, name, body, draft, preRelease, generateReleaseNotes); + } + } +} diff --git a/buildSrc/src/main/java/s101/S101Configurer.java b/buildSrc/src/main/java/s101/S101Configurer.java index 414e4885358..1575f47fbdc 100644 --- a/buildSrc/src/main/java/s101/S101Configurer.java +++ b/buildSrc/src/main/java/s101/S101Configurer.java @@ -164,14 +164,18 @@ private String installBuildTool(File installationDirectory, File configurationDi String source = "https://structure101.com/binaries/v6"; try (final WebClient webClient = new WebClient()) { HtmlPage page = webClient.getPage(source); + Matcher matcher = null; for (HtmlAnchor anchor : page.getAnchors()) { - Matcher matcher = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute()); - if (matcher.find()) { - copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2)); - return matcher.group(2); + Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute()); + if (candidate.find()) { + matcher = candidate; } } - return null; + if (matcher == null) { + return null; + } + copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2)); + return matcher.group(2); } catch (Exception ex) { throw new RuntimeException(ex); } diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java b/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java index e8ad62118e9..dff022f3693 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. You may obtain a copy of @@ -16,6 +16,11 @@ package io.spring.gradle.convention; +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + import io.spring.gradle.IncludeRepoTask; import org.apache.commons.io.FileUtils; import org.gradle.api.Project; @@ -24,8 +29,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import java.util.Arrays; - import static org.assertj.core.api.Assertions.assertThat; class IncludeCheckRemotePluginTest { @@ -68,6 +71,40 @@ void applyWhenExtensionPropertiesTasksThenCreateCheckRemoteWithProvidedTasks() { assertThat(checkRemote.getTasks()).containsExactly("clean", "build", "test"); } + @Test + void applyWhenExtensionPropertiesInitScriptsThenCreateCheckRemoteWithProvidedTasks() { + this.rootProject = ProjectBuilder.builder().build(); + this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class); + this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class, + (includeCheckRemoteExtension) -> { + includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository"); + includeCheckRemoteExtension.setProperty("ref", "main"); + includeCheckRemoteExtension.setProperty("initScripts", Arrays.asList("spring-security-ci.gradle")); + }); + + GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get(); + assertThat(checkRemote.getStartParameter().getAllInitScripts()).extracting(File::getName).containsExactly("spring-security-ci.gradle"); + } + + @Test + void applyWhenExtensionPropertiesBuildPropertiesThenCreateCheckRemoteWithProvidedTasks() { + Map projectProperties = new HashMap<>(); + projectProperties.put("localRepositoryPath", "~/local/repository"); + projectProperties.put("anotherProperty", "some_value"); + this.rootProject = ProjectBuilder.builder().build(); + this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class); + this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class, + (includeCheckRemoteExtension) -> { + includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository"); + includeCheckRemoteExtension.setProperty("ref", "main"); + includeCheckRemoteExtension.setProperty("projectProperties", projectProperties); + }); + + GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get(); + assertThat(checkRemote.getStartParameter().getProjectProperties()).containsEntry("localRepositoryPath", "~/local/repository") + .containsEntry("anotherProperty", "some_value"); + } + @Test void applyWhenExtensionPropertiesThenRegisterIncludeRepoTaskWithExtensionProperties() { this.rootProject = ProjectBuilder.builder().build(); diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java b/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java index 2bad49c8a3a..f1048dbbab6 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java @@ -107,7 +107,7 @@ public void applyWhenIsReleaseWithForceLocalThenShouldIncludeReleaseAndLocalRepo 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 void applyWhenIsReleaseWithForceMilestoneAndLocalThenShouldIncludeMilesto 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(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/"); - assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) .isEqualTo("https://repo.spring.io/snapshot/"); - assertThat(((MavenArtifactRepository) repositories.get(3)).getUrl().toString()) + assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) .isEqualTo("https://repo.spring.io/milestone/"); } private void assertMilestoneRepository(RepositoryHandler repositories) { - assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(5); + 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(4); + 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://jcenter.bintray.com/"); - assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) .isEqualTo("https://repo.spring.io/release/"); } diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java b/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java index f4e8c11c565..da50eef986a 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java @@ -58,7 +58,7 @@ public void createWhenValidThenNoException() throws Exception { 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 void createWhenValidThenNoException() throws Exception { 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" + "}"); } diff --git a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java index 183cf09d5ab..b9b0764ee53 100644 --- a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,15 +1,16 @@ package io.spring.gradle.github.milestones; +import java.util.concurrent.TimeUnit; + import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.gradle.github.milestones.GitHubMilestoneApi; -import org.springframework.gradle.github.milestones.RepositoryRef; -import java.util.concurrent.TimeUnit; +import org.springframework.gradle.github.RepositoryRef; +import org.springframework.gradle.github.milestones.GitHubMilestoneApi; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java new file mode 100644 index 00000000000..98eedad65ea --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -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(); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index b4072c079eb..0a1a293ab04 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,5 +1,7 @@ package org.springframework.gradle.github.milestones; +import java.util.concurrent.TimeUnit; + import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -7,7 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.concurrent.TimeUnit; +import org.springframework.gradle.github.RepositoryRef; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java new file mode 100644 index 00000000000..6ac79557222 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.util.concurrent.TimeUnit; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.gradle.github.RepositoryRef; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Steve Riesenberg + */ +public class GitHubReleaseApiTests { + private GitHubReleaseApi github; + + private RepositoryRef repository = new RepositoryRef("spring-projects", "spring-security"); + + private MockWebServer server; + + private String baseUrl; + + @BeforeEach + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.github = new GitHubReleaseApi("mock-oauth-token"); + this.baseUrl = this.server.url("/api").toString(); + this.github.setBaseUrl(this.baseUrl); + } + + @AfterEach + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void publishReleaseWhenValidParametersThenSuccess() throws Exception { + String responseJson = "{\n" + + " \"url\": \"https://api.github.com/spring-projects/spring-security/releases/1\",\n" + + " \"html_url\": \"https://github.com/spring-projects/spring-security/releases/tags/v1.0.0\",\n" + + " \"assets_url\": \"https://api.github.com/spring-projects/spring-security/releases/1/assets\",\n" + + " \"upload_url\": \"https://uploads.github.com/spring-projects/spring-security/releases/1/assets{?name,label}\",\n" + + " \"tarball_url\": \"https://api.github.com/spring-projects/spring-security/tarball/v1.0.0\",\n" + + " \"zipball_url\": \"https://api.github.com/spring-projects/spring-security/zipball/v1.0.0\",\n" + + " \"discussion_url\": \"https://github.com/spring-projects/spring-security/discussions/90\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDc6UmVsZWFzZTE=\",\n" + + " \"tag_name\": \"v1.0.0\",\n" + + " \"target_commitish\": \"main\",\n" + + " \"name\": \"v1.0.0\",\n" + + " \"body\": \"Description of the release\",\n" + + " \"draft\": false,\n" + + " \"prerelease\": false,\n" + + " \"created_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"published_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"author\": {\n" + + " \"login\": \"sjohnr\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/sjohnr\",\n" + + " \"html_url\": \"https://github.com/sjohnr\",\n" + + " \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " },\n" + + " \"assets\": [\n" + + " {\n" + + " \"url\": \"https://api.github.com/spring-projects/spring-security/releases/assets/1\",\n" + + " \"browser_download_url\": \"https://github.com/spring-projects/spring-security/releases/download/v1.0.0/example.zip\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDEyOlJlbGVhc2VBc3NldDE=\",\n" + + " \"name\": \"example.zip\",\n" + + " \"label\": \"short description\",\n" + + " \"state\": \"uploaded\",\n" + + " \"content_type\": \"application/zip\",\n" + + " \"size\": 1024,\n" + + " \"download_count\": 42,\n" + + " \"created_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"updated_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"uploader\": {\n" + + " \"login\": \"sjohnr\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/sjohnr\",\n" + + " \"html_url\": \"https://github.com/sjohnr\",\n" + + " \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + this.github.publishRelease(this.repository, Release.tag("1.0.0").build()); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); + assertThat(recordedRequest.getBody().toString()).isEqualTo("{\"tag_name\":\"1.0.0\"}"); + } + + @Test + public void publishReleaseWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.publishRelease(this.repository, Release.tag("1.0.0").build())); + } +} diff --git a/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle b/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle index 732278d03b5..a0526f319e2 100644 --- a/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle +++ b/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle @@ -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' } \ No newline at end of file diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle index 8b3d4630f75..ed4331c3a40 100644 --- a/cas/spring-security-cas.gradle +++ b/cas/spring-security-cas.gradle @@ -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" diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java index 2352887b1f7..8f4e7c0d109 100644 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java +++ b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java @@ -42,6 +42,8 @@ import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -205,6 +207,8 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil private AuthenticationFailureHandler proxyFailureHandler = new SimpleUrlAuthenticationFailureHandler(); + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + public CasAuthenticationFilter() { super("/login/cas"); setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler()); @@ -223,6 +227,7 @@ protected final void successfulAuthentication(HttpServletRequest request, HttpSe SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } @@ -246,7 +251,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ this.logger.debug("Failed to obtain an artifact (cas ticket)"); password = ""; } - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); + UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, + password); authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); return this.getAuthenticationManager().authenticate(authRequest); } @@ -274,6 +280,18 @@ protected boolean requiresAuthentication(HttpServletRequest request, HttpServlet return result; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + /** * Sets the {@link AuthenticationFailureHandler} for proxy requests. * @param proxyFailureHandler diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java index 242d32a730d..b469d73af51 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java @@ -87,8 +87,8 @@ public void statefulAuthenticationIsSuccessful() throws Exception { cap.setServiceProperties(makeServiceProperties()); cap.setTicketValidator(new MockTicketValidator(true)); cap.afterPropertiesSet(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .unauthenticated(CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123"); token.setDetails("details"); Authentication result = cap.authenticate(token); // Confirm ST-123 was NOT added to the cache @@ -120,8 +120,8 @@ public void statelessAuthenticationIsSuccessful() throws Exception { cap.setTicketValidator(new MockTicketValidator(true)); cap.setServiceProperties(makeServiceProperties()); cap.afterPropertiesSet(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .unauthenticated(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456"); token.setDetails("details"); Authentication result = cap.authenticate(token); // Confirm ST-456 was added to the cache @@ -157,8 +157,8 @@ public void authenticateAllNullService() throws Exception { cap.setServiceProperties(serviceProperties); cap.afterPropertiesSet(); String ticket = "ST-456"; - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .unauthenticated(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket); Authentication result = cap.authenticate(token); } @@ -178,8 +178,8 @@ public void authenticateAllAuthenticationIsSuccessful() throws Exception { cap.setServiceProperties(serviceProperties); cap.afterPropertiesSet(); String ticket = "ST-456"; - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .unauthenticated(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket); Authentication result = cap.authenticate(token); verify(validator).validate(ticket, serviceProperties.getService()); serviceProperties.setAuthenticateAllArtifacts(true); @@ -211,8 +211,8 @@ public void missingTicketIdIsDetected() throws Exception { cap.setTicketValidator(new MockTicketValidator(true)); cap.setServiceProperties(makeServiceProperties()); cap.afterPropertiesSet(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, ""); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .unauthenticated(CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, ""); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token)); } @@ -314,8 +314,8 @@ public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPr cap.setTicketValidator(new MockTicketValidator(true)); cap.setServiceProperties(makeServiceProperties()); cap.afterPropertiesSet(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("some_normal_user", - "password", AuthorityUtils.createAuthorityList("ROLE_A")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .authenticated("some_normal_user", "password", AuthorityUtils.createAuthorityList("ROLE_A")); assertThat(cap.authenticate(token)).isNull(); } diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java index 8ac076830be..364240a3a9a 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java @@ -121,8 +121,8 @@ public void testNotEqualsDueToDifferentAuthenticationClass() { final Assertion assertion = new AssertionImpl("test"); CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, makeUserDetails(), assertion); - UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test", "Password", - this.ROLES); + UsernamePasswordAuthenticationToken token2 = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", this.ROLES); assertThat(!token1.equals(token2)).isTrue(); } diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java index fab4d2ed1d9..c1338cc5ae9 100644 --- a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java +++ b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java @@ -21,6 +21,7 @@ import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -32,12 +33,15 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.context.SecurityContextRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -182,6 +186,38 @@ public void testDoFilterAuthenticateAll() throws Exception { verify(successHandler).onAuthenticationSuccess(request, response, authentication); } + @Test + public void testSecurityContextHolder() throws Exception { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + AuthenticationManager manager = mock(AuthenticationManager.class); + Authentication authentication = new TestingAuthenticationToken("un", "pwd", "ROLE_USER"); + given(manager.authenticate(any(Authentication.class))).willReturn(authentication); + ServiceProperties serviceProperties = new ServiceProperties(); + serviceProperties.setAuthenticateAllArtifacts(true); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("ticket", "ST-1-123"); + request.setServletPath("/authenticate"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + CasAuthenticationFilter filter = new CasAuthenticationFilter(); + filter.setServiceProperties(serviceProperties); + filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); + filter.setAuthenticationManager(manager); + filter.setSecurityContextRepository(securityContextRepository); + filter.afterPropertiesSet(); + filter.doFilter(request, response, chain); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull() + .withFailMessage("Authentication should not be null"); + verify(chain).doFilter(request, response); + // validate for when the filterProcessUrl matches + filter.setFilterProcessesUrl(request.getServletPath()); + SecurityContextHolder.clearContext(); + filter.doFilter(request, response, chain); + ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); + verify(securityContextRepository).saveContext(contextArg.capture(), eq(request), eq(response)); + assertThat(contextArg.getValue().getAuthentication().getPrincipal()).isEqualTo(authentication.getName()); + } + // SEC-1592 @Test public void testChainNotInvokedForProxyReceptor() throws Exception { diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index a54edbd15c8..5c634a1abb5 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -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' @@ -74,13 +76,20 @@ dependencies { testImplementation "org.apache.directory.server:apacheds-protocol-ldap" testImplementation "org.apache.directory.server:apacheds-server-jndi" testImplementation 'org.apache.directory.shared:shared-ldap' + testImplementation "com.unboundid:unboundid-ldapsdk" 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 +98,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' @@ -105,7 +113,6 @@ dependencies { testRuntimeOnly 'org.hsqldb:hsqldb' } - rncToXsd { rncDir = file('src/main/resources/org/springframework/security/config/') xsdDir = rncDir @@ -122,3 +129,37 @@ tasks.withType(KotlinCompile).configureEach { } build.dependsOn rncToXsd + +compileTestJava { + exclude "org/springframework/security/config/annotation/web/configurers/saml2/**", "org/springframework/security/config/http/Saml2*" +} + +task compileSaml2TestJava(type: JavaCompile) { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } + source = sourceSets.test.java.srcDirs + include "org/springframework/security/config/annotation/web/configurers/saml2/**", "org/springframework/security/config/http/Saml2*" + classpath = sourceSets.test.compileClasspath + destinationDirectory = new File("${buildDir}/classes/java/test") + options.sourcepath = sourceSets.test.java.getSourceDirectories() +} + +task saml2Tests(type: Test) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(11) + } + filter { + includeTestsMatching "org.springframework.security.config.annotation.web.configurers.saml2.*" + } + useJUnitPlatform() + dependsOn compileSaml2TestJava +} + +test { + shouldRunAfter saml2Tests +} + +tasks.named('check') { + dependsOn saml2Tests +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java new file mode 100644 index 00000000000..d20b3e302cc --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class EmbeddedLdapServerContextSourceFactoryBeanITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void contextSourceFactoryBeanWhenEmbeddedServerThenAuthenticates() throws Exception { + this.spring.register(FromEmbeddedLdapServerConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenPortZeroThenAuthenticates() throws Exception { + this.spring.register(PortZeroConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenCustomLdifAndRootThenAuthenticates() throws Exception { + this.spring.register(CustomLdifAndRootConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("pg").password("password")).andExpect(authenticated().withUsername("pg")); + } + + @Test + public void contextSourceFactoryBeanWhenCustomManagerDnThenAuthenticates() throws Exception { + this.spring.register(CustomManagerDnConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenManagerDnAndNoPasswordThenException() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> this.spring.register(CustomManagerDnNoPasswordConfig.class).autowire()) + .withRootCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("managerPassword is required if managerDn is supplied"); + } + + @EnableWebSecurity + static class FromEmbeddedLdapServerConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer(); + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class PortZeroConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setPort(0); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomLdifAndRootConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setLdif("classpath*:test-server2.xldif"); + factoryBean.setRoot("dc=monkeymachine,dc=co,dc=uk"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=gorillas"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomManagerDnConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setManagerDn("uid=admin,ou=system"); + factoryBean.setManagerPassword("secret"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomManagerDnNoPasswordConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setManagerDn("uid=admin,ou=system"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java new file mode 100644 index 00000000000..2b333441c09 --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.mock; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class LdapBindAuthenticationManagerFactoryITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void authenticationManagerFactoryWhenFromContextSourceThenAuthenticates() throws Exception { + this.spring.register(FromContextSourceConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void ldapAuthenticationProviderCustomLdapAuthoritiesPopulator() throws Exception { + CustomAuthoritiesPopulatorConfig.LAP = new DefaultLdapAuthoritiesPopulator(mock(LdapContextSource.class), + null) { + @Override + protected Set getAdditionalRoles(DirContextOperations user, String username) { + return new HashSet<>(AuthorityUtils.createAuthorityList("ROLE_EXTRA")); + } + }; + + this.spring.register(CustomAuthoritiesPopulatorConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")).andExpect( + authenticated().withAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_EXTRA")))); + } + + @Test + public void authenticationManagerFactoryWhenCustomAuthoritiesMapperThenUsed() throws Exception { + CustomAuthoritiesMapperConfig.AUTHORITIES_MAPPER = ((authorities) -> AuthorityUtils + .createAuthorityList("ROLE_CUSTOM")); + + this.spring.register(CustomAuthoritiesMapperConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")).andExpect( + authenticated().withAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_CUSTOM")))); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserDetailsContextMapperThenUsed() throws Exception { + CustomUserDetailsContextMapperConfig.CONTEXT_MAPPER = new UserDetailsContextMapper() { + @Override + public UserDetails mapUserFromContext(DirContextOperations ctx, String username, + Collection authorities) { + return User.withUsername("other").password("password").roles("USER").build(); + } + + @Override + public void mapUserToContext(UserDetails user, DirContextAdapter ctx) { + } + }; + + this.spring.register(CustomUserDetailsContextMapperConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("other")); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserDnPatternsThenUsed() throws Exception { + this.spring.register(CustomUserDnPatternsConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserSearchThenUsed() throws Exception { + this.spring.register(CustomUserSearchConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @EnableWebSecurity + static class FromContextSourceConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomAuthoritiesMapperConfig extends BaseLdapServerConfig { + + static GrantedAuthoritiesMapper AUTHORITIES_MAPPER; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setAuthoritiesMapper(AUTHORITIES_MAPPER); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomAuthoritiesPopulatorConfig extends BaseLdapServerConfig { + + static LdapAuthoritiesPopulator LAP; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setLdapAuthoritiesPopulator(LAP); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserDetailsContextMapperConfig extends BaseLdapServerConfig { + + static UserDetailsContextMapper CONTEXT_MAPPER; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setUserDetailsContextMapper(CONTEXT_MAPPER); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserDnPatternsConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserSearchConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserSearchFilter("uid={0}"); + factory.setUserSearchBase("ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + abstract static class BaseLdapServerConfig implements DisposableBean { + + private ApacheDSContainer container; + + @Bean + ApacheDSContainer ldapServer() throws Exception { + this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/test-server.ldif"); + this.container.setPort(0); + return this.container; + } + + @Bean + BaseLdapPathContextSource contextSource(ApacheDSContainer container) { + int port = container.getLocalPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Override + public void destroy() { + this.container.stop(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java new file mode 100644 index 00000000000..350cf8405ce --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class LdapPasswordComparisonAuthenticationManagerFactoryITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void authenticationManagerFactoryWhenCustomPasswordEncoderThenUsed() throws Exception { + this.spring.register(CustomPasswordEncoderConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bcrypt").password("password")) + .andExpect(authenticated().withUsername("bcrypt")); + } + + @Test + public void authenticationManagerFactoryWhenCustomPasswordAttributeThenUsed() throws Exception { + this.spring.register(CustomPasswordAttributeConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bob")).andExpect(authenticated().withUsername("bob")); + } + + @EnableWebSecurity + static class CustomPasswordEncoderConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, new BCryptPasswordEncoder()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomPasswordAttributeConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setPasswordAttribute("uid"); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + abstract static class BaseLdapServerConfig implements DisposableBean { + + private ApacheDSContainer container; + + @Bean + ApacheDSContainer ldapServer() throws Exception { + this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/test-server.ldif"); + this.container.setPort(0); + return this.container; + } + + @Bean + BaseLdapPathContextSource contextSource(ApacheDSContainer container) { + int port = container.getLocalPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Override + public void destroy() { + this.container.stop(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java index 43b29094777..d3b0f5c20c5 100644 --- a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java @@ -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. @@ -56,7 +56,7 @@ public void simpleProviderAuthenticatesCorrectly() { AuthenticationManager authenticationManager = this.appCtx.getBean(BeanIds.AUTHENTICATION_MANAGER, AuthenticationManager.class); Authentication auth = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("ben", "benspassword")); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword")); UserDetails ben = (UserDetails) auth.getPrincipal(); assertThat(ben.getAuthorities()).hasSize(3); } @@ -89,7 +89,7 @@ public void supportsPasswordComparisonAuthentication() { AuthenticationManager authenticationManager = this.appCtx.getBean(BeanIds.AUTHENTICATION_MANAGER, AuthenticationManager.class); Authentication auth = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("ben", "benspassword")); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword")); assertThat(auth).isNotNull(); } @@ -104,7 +104,8 @@ public void supportsPasswordComparisonAuthenticationWithPasswordEncoder() { AuthenticationManager authenticationManager = this.appCtx.getBean(BeanIds.AUTHENTICATION_MANAGER, AuthenticationManager.class); - Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("ben", "ben")); + Authentication auth = authenticationManager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("ben", "ben")); assertThat(auth).isNotNull(); } @@ -121,7 +122,7 @@ public void supportsCryptoPasswordEncoder() { AuthenticationManager authenticationManager = this.appCtx.getBean(BeanIds.AUTHENTICATION_MANAGER, AuthenticationManager.class); Authentication auth = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("bcrypt", "password")); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("bcrypt", "password")); assertThat(auth).isNotNull(); } diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index 0b79c47d65d..ac6cb1fe633 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -138,4 +138,10 @@ public abstract class Elements { public static final String PASSWORD_MANAGEMENT = "password-management"; + public static final String RELYING_PARTY_REGISTRATIONS = "relying-party-registrations"; + + public static final String SAML2_LOGIN = "saml2-login"; + + public static final String SAML2_LOGOUT = "saml2-logout"; + } diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index d3dd04a7d9c..6ef3b723455 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2020 the original author or authors. + * Copyright 2009-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. @@ -47,6 +47,7 @@ import org.springframework.security.config.method.MethodSecurityBeanDefinitionParser; import org.springframework.security.config.method.MethodSecurityMetadataSourceBeanDefinitionParser; import org.springframework.security.config.oauth2.client.ClientRegistrationsBeanDefinitionParser; +import org.springframework.security.config.saml2.RelyingPartyRegistrationsBeanDefinitionParser; import org.springframework.security.config.websocket.WebSocketMessageBrokerSecurityBeanDefinitionParser; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.ClassUtils; @@ -94,7 +95,7 @@ public BeanDefinition parse(Element element, ParserContext pc) { if (!namespaceMatchesVersion(element)) { pc.getReaderContext().fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or " + "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema " - + "with Spring Security 5.6. Please update your schema declarations to the 5.6 schema.", element); + + "with Spring Security 5.8. Please update your schema declarations to the 5.8 schema.", element); } String name = pc.getDelegate().getLocalName(element); BeanDefinitionParser parser = this.parsers.get(name); @@ -190,6 +191,7 @@ private void loadWebParsers() { this.parsers.put(Elements.FILTER_CHAIN, new FilterChainBeanDefinitionParser()); this.filterChainMapBDD = new FilterChainMapBeanDefinitionDecorator(); this.parsers.put(Elements.CLIENT_REGISTRATIONS, new ClientRegistrationsBeanDefinitionParser()); + this.parsers.put(Elements.RELYING_PARTY_REGISTRATIONS, new RelyingPartyRegistrationsBeanDefinitionParser()); } private void loadWebSocketParsers() { @@ -215,7 +217,7 @@ private boolean namespaceMatchesVersion(Element element) { private boolean matchesVersionInternal(Element element) { String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"); - return schemaLocation.matches("(?m).*spring-security-5\\.6.*.xsd.*") + return schemaLocation.matches("(?m).*spring-security-5\\.8.*.xsd.*") || schemaLocation.matches("(?m).*spring-security.xsd.*") || !schemaLocation.matches("(?m).*spring-security.*"); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index d6dba7069ef..8786279b0a6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -17,16 +17,16 @@ package org.springframework.security.config.annotation.method.configuration; import org.springframework.aop.Advisor; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationEventPublisher; +import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; @@ -45,59 +45,64 @@ */ @Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) -final class PrePostMethodSecurityConfiguration implements ApplicationContextAware { +final class PrePostMethodSecurityConfiguration { private final PreFilterAuthorizationMethodInterceptor preFilterAuthorizationMethodInterceptor = new PreFilterAuthorizationMethodInterceptor(); + private final AuthorizationManagerBeforeMethodInterceptor preAuthorizeAuthorizationMethodInterceptor; + private final PreAuthorizeAuthorizationManager preAuthorizeAuthorizationManager = new PreAuthorizeAuthorizationManager(); + private final AuthorizationManagerAfterMethodInterceptor postAuthorizeAuthorizaitonMethodInterceptor; + private final PostAuthorizeAuthorizationManager postAuthorizeAuthorizationManager = new PostAuthorizeAuthorizationManager(); private final PostFilterAuthorizationMethodInterceptor postFilterAuthorizationMethodInterceptor = new PostFilterAuthorizationMethodInterceptor(); private final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - private boolean customMethodSecurityExpressionHandler = false; + @Autowired + PrePostMethodSecurityConfiguration(ApplicationContext context) { + this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); + this.preAuthorizeAuthorizationMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(this.preAuthorizeAuthorizationManager); + this.postAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); + this.postAuthorizeAuthorizaitonMethodInterceptor = AuthorizationManagerAfterMethodInterceptor + .postAuthorize(this.postAuthorizeAuthorizationManager); + this.preFilterAuthorizationMethodInterceptor.setExpressionHandler(this.expressionHandler); + this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(this.expressionHandler); + this.expressionHandler.setApplicationContext(context); + AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(context); + this.preAuthorizeAuthorizationMethodInterceptor.setAuthorizationEventPublisher(publisher); + this.postAuthorizeAuthorizaitonMethodInterceptor.setAuthorizationEventPublisher(publisher); + } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor preFilterAuthorizationMethodInterceptor() { - if (!this.customMethodSecurityExpressionHandler) { - this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); - } return this.preFilterAuthorizationMethodInterceptor; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor preAuthorizeAuthorizationMethodInterceptor() { - if (!this.customMethodSecurityExpressionHandler) { - this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); - } - return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(this.preAuthorizeAuthorizationManager); + return this.preAuthorizeAuthorizationMethodInterceptor; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor postAuthorizeAuthorizationMethodInterceptor() { - if (!this.customMethodSecurityExpressionHandler) { - this.postAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); - } - return AuthorizationManagerAfterMethodInterceptor.postAuthorize(this.postAuthorizeAuthorizationManager); + return this.postAuthorizeAuthorizaitonMethodInterceptor; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor postFilterAuthorizationMethodInterceptor() { - if (!this.customMethodSecurityExpressionHandler) { - this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(this.expressionHandler); - } return this.postFilterAuthorizationMethodInterceptor; } @Autowired(required = false) void setMethodSecurityExpressionHandler(MethodSecurityExpressionHandler methodSecurityExpressionHandler) { - this.customMethodSecurityExpressionHandler = true; this.preFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler); this.preAuthorizeAuthorizationManager.setExpressionHandler(methodSecurityExpressionHandler); this.postAuthorizeAuthorizationManager.setExpressionHandler(methodSecurityExpressionHandler); @@ -109,9 +114,10 @@ void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaul this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); } - @Override - public void setApplicationContext(ApplicationContext context) throws BeansException { - this.expressionHandler.setApplicationContext(context); + @Autowired(required = false) + void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { + this.preAuthorizeAuthorizationMethodInterceptor.setAuthorizationEventPublisher(eventPublisher); + this.postAuthorizeAuthorizaitonMethodInterceptor.setAuthorizationEventPublisher(eventPublisher); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index d2cea903a43..473b9ef4ada 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -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. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java index 475f2de2f50..14b30145888 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java @@ -42,6 +42,8 @@ import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; +import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.SessionManagementFilter; /** @@ -124,6 +126,8 @@ public interface HttpSecurityBuilder> * The ordering of the Filters is: * *
    + *
  • {@link ForceEagerSessionCreationFilter}
  • + *
  • {@link DisableEncodeUrlFilter}
  • *
  • {@link ChannelProcessingFilter}
  • *
  • {@link SecurityContextPersistenceFilter}
  • *
  • {@link LogoutFilter}
  • diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index 13248b7e679..75fcc72c56c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java @@ -37,6 +37,7 @@ import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.security.web.csrf.CsrfFilter; @@ -45,6 +46,8 @@ import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; +import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.web.filter.CorsFilter; @@ -67,9 +70,12 @@ final class FilterOrderRegistration { FilterOrderRegistration() { Step order = new Step(INITIAL_ORDER, ORDER_STEP); + put(DisableEncodeUrlFilter.class, order.next()); + put(ForceEagerSessionCreationFilter.class, order.next()); put(ChannelProcessingFilter.class, order.next()); order.next(); // gh-8105 put(WebAsyncManagerIntegrationFilter.class, order.next()); + put(SecurityContextHolderFilter.class, order.next()); put(SecurityContextPersistenceFilter.class, order.next()); put(HeaderWriterFilter.class, order.next()); put(CorsFilter.class, order.next()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 14d32ecd76c..8a627ef3108 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -2889,8 +2889,15 @@ protected void beforeConfigure() throws Exception { } } + @SuppressWarnings("unchecked") @Override protected DefaultSecurityFilterChain performBuild() { + ExpressionUrlAuthorizationConfigurer expressionConfigurer = getConfigurer( + ExpressionUrlAuthorizationConfigurer.class); + AuthorizeHttpRequestsConfigurer httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class); + boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null; + Assert.state((expressionConfigurer == null && httpConfigurer == null) || oneConfigurerPresent, + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."); this.filters.sort(OrderComparator.INSTANCE); List sortedFilters = new ArrayList<>(this.filters.size()); for (Filter filter : this.filters) { @@ -3283,20 +3290,26 @@ private mvcMatchers; + /** * Creates a new instance * @param context the {@link ApplicationContext} to use - * @param matchers the {@link MvcRequestMatcher} instances to set the servlet path - * on if {@link #servletPath(String)} is set. + * @param mvcMatchers the {@link MvcRequestMatcher} instances to set the servlet + * path on if {@link #servletPath(String)} is set. + * @param allMatchers the {@link RequestMatcher} instances to continue the + * configuration */ - private MvcMatchersRequestMatcherConfigurer(ApplicationContext context, List matchers) { + private MvcMatchersRequestMatcherConfigurer(ApplicationContext context, List mvcMatchers, + List allMatchers) { super(context); - this.matchers = new ArrayList<>(matchers); + this.mvcMatchers = new ArrayList<>(mvcMatchers); + this.matchers = allMatchers; } public RequestMatcherConfigurer servletPath(String servletPath) { - for (RequestMatcher matcher : this.matchers) { - ((MvcRequestMatcher) matcher).setServletPath(servletPath); + for (MvcRequestMatcher matcher : this.mvcMatchers) { + matcher.setServletPath(servletPath); } return this; } @@ -3321,7 +3334,7 @@ public class RequestMatcherConfigurer extends AbstractRequestMatcherRegistry mvcMatchers = createMvcMatchers(method, mvcPatterns); setMatchers(mvcMatchers); - return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers); + return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers, this.matchers); } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index f0395b840ea..d273ad0b687 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -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. @@ -20,6 +20,7 @@ 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.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.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.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 @@ * @see WebSecurityConfiguration */ public final class WebSecurity extends AbstractConfiguredSecurityBuilder - implements SecurityBuilder, ApplicationContextAware { + implements SecurityBuilder, ApplicationContextAware, ServletContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -108,6 +115,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder { }; + private ServletContext servletContext; + /** * Creates a new instance * @param objectPostProcessor the {@link ObjectPostProcessor} to use @@ -252,6 +261,8 @@ public WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() { * {@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; @@ -268,6 +279,19 @@ public WebSecurity postBuildAction(Runnable postBuildAction) { return this; } + /** + * Sets the handler to handle + * {@link org.springframework.security.web.firewall.RequestRejectedException} + * @param requestRejectedHandler + * @return the {@link WebSecurity} for further customizations + * @since 5.7 + */ + public WebSecurity requestRejectedHandler(RequestRejectedHandler requestRejectedHandler) { + Assert.notNull(requestRejectedHandler, "requestRejectedHandler cannot be null"); + this.requestRejectedHandler = requestRejectedHandler; + return this; + } + @Override protected Filter performBuild() throws Exception { Assert.state(!this.securityFilterChainBuilders.isEmpty(), @@ -278,11 +302,24 @@ protected Filter performBuild() throws Exception { + ".addSecurityFilterChainBuilder directly"); int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size(); List securityFilterChains = new ArrayList<>(chainSize); + List>> 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 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 +343,29 @@ protected Filter performBuild() throws Exception { return result; } + private RequestMatcherEntry> getRequestMatcherPrivilegeEvaluatorsEntry( + SecurityFilterChain securityFilterChain) { + List 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 authorizationManager = ((AuthorizationFilter) filter) + .getAuthorizationManager(); + AuthorizationManagerWebInvocationPrivilegeEvaluator evaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator( + authorizationManager); + evaluator.setServletContext(this.servletContext); + privilegeEvaluators.add(evaluator); + } + } + return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext); @@ -333,6 +393,11 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } } + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + /** * An {@link IgnoredRequestConfigurer} that allows optionally configuring the * {@link MvcRequestMatcher#setMethod(HttpMethod)} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 2aad5f658af..468ba74bf5e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -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. @@ -17,6 +17,7 @@ package org.springframework.security.config.annotation.web.configuration; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; @@ -24,11 +25,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; @@ -97,6 +100,7 @@ HttpSecurity httpSecurity() throws Exception { .apply(new DefaultLoginPageConfigurer<>()); http.logout(withDefaults()); // @formatter:on + applyDefaultConfigurers(http); return http; } @@ -105,6 +109,15 @@ private AuthenticationManager authenticationManager() throws Exception { : this.authenticationConfiguration.getAuthenticationManager(); } + private void applyDefaultConfigurers(HttpSecurity http) throws Exception { + ClassLoader classLoader = this.context.getClassLoader(); + List defaultHttpConfigurers = SpringFactoriesLoader + .loadFactories(AbstractHttpConfigurer.class, classLoader); + for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) { + http.apply(configurer); + } + } + private Map, Object> createSharedObjects() { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java index 2783cb358bc..41ab29c93a7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java @@ -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.config.annotation.web.configuration; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -67,17 +71,22 @@ static class SecurityReactorContextSubscriberRegistrar implements InitializingBe private static final String SECURITY_REACTOR_CONTEXT_OPERATOR_KEY = "org.springframework.security.SECURITY_REACTOR_CONTEXT_OPERATOR"; + private static final Map> CONTEXT_ATTRIBUTE_VALUE_LOADERS = new HashMap<>(); + + static { + CONTEXT_ATTRIBUTE_VALUE_LOADERS.put(HttpServletRequest.class, + SecurityReactorContextSubscriberRegistrar::getRequest); + CONTEXT_ATTRIBUTE_VALUE_LOADERS.put(HttpServletResponse.class, + SecurityReactorContextSubscriberRegistrar::getResponse); + CONTEXT_ATTRIBUTE_VALUE_LOADERS.put(Authentication.class, + SecurityReactorContextSubscriberRegistrar::getAuthentication); + } + @Override public void afterPropertiesSet() throws Exception { Function, ? extends Publisher> lifter = Operators .liftPublisher((pub, sub) -> createSubscriberIfNecessary(sub)); - Hooks.onLastOperator(SECURITY_REACTOR_CONTEXT_OPERATOR_KEY, (pub) -> { - if (!contextAttributesAvailable()) { - // No need to decorate so return original Publisher - return pub; - } - return lifter.apply(pub); - }); + Hooks.onLastOperator(SECURITY_REACTOR_CONTEXT_OPERATOR_KEY, lifter::apply); } @Override @@ -93,36 +102,30 @@ CoreSubscriber createSubscriberIfNecessary(CoreSubscriber delegate) { return new SecurityReactorContextSubscriber<>(delegate, getContextAttributes()); } - private static boolean contextAttributesAvailable() { - return SecurityContextHolder.getContext().getAuthentication() != null - || RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes; + private static Map getContextAttributes() { + return new LoadingMap<>(CONTEXT_ATTRIBUTE_VALUE_LOADERS); } - private static Map getContextAttributes() { - HttpServletRequest servletRequest = null; - HttpServletResponse servletResponse = null; + private static HttpServletRequest getRequest() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; - servletRequest = servletRequestAttributes.getRequest(); - servletResponse = servletRequestAttributes.getResponse(); // possible null + return servletRequestAttributes.getRequest(); } - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null && servletRequest == null) { - return Collections.emptyMap(); - } - Map contextAttributes = new HashMap<>(); - if (servletRequest != null) { - contextAttributes.put(HttpServletRequest.class, servletRequest); - } - if (servletResponse != null) { - contextAttributes.put(HttpServletResponse.class, servletResponse); - } - if (authentication != null) { - contextAttributes.put(Authentication.class, authentication); + return null; + } + + private static HttpServletResponse getResponse() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes instanceof ServletRequestAttributes) { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getResponse(); // possible null } + return null; + } - return contextAttributes; + private static Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); } } @@ -175,4 +178,112 @@ public void onComplete() { } + /** + * A map that computes each value when {@link #get} is invoked + */ + static class LoadingMap implements Map { + + private final Map loaded = new ConcurrentHashMap<>(); + + private final Map> loaders; + + LoadingMap(Map> loaders) { + this.loaders = Collections.unmodifiableMap(new HashMap<>(loaders)); + } + + @Override + public int size() { + return this.loaders.size(); + } + + @Override + public boolean isEmpty() { + return this.loaders.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.loaders.containsKey(key); + } + + @Override + public Set keySet() { + return this.loaders.keySet(); + } + + @Override + public V get(Object key) { + if (!this.loaders.containsKey(key)) { + throw new IllegalArgumentException( + "This map only supports the following keys: " + this.loaders.keySet()); + } + return this.loaded.computeIfAbsent((K) key, (k) -> this.loaders.get(k).get()); + } + + @Override + public V put(K key, V value) { + if (!this.loaders.containsKey(key)) { + throw new IllegalArgumentException( + "This map only supports the following keys: " + this.loaders.keySet()); + } + return this.loaded.put(key, value); + } + + @Override + public V remove(Object key) { + if (!this.loaders.containsKey(key)) { + throw new IllegalArgumentException( + "This map only supports the following keys: " + this.loaders.keySet()); + } + return this.loaded.remove(key); + } + + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + this.loaded.clear(); + } + + @Override + public boolean containsValue(Object value) { + return this.loaded.containsValue(value); + } + + @Override + public Collection values() { + return this.loaded.values(); + } + + @Override + public Set> entrySet() { + return this.loaded.entrySet(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LoadingMap that = (LoadingMap) o; + + return this.loaded.equals(that.loaded); + } + + @Override + public int hashCode() { + return this.loaded.hashCode(); + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 45bf5e5f1e7..1af50254c94 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -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. @@ -24,7 +24,6 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; @@ -128,8 +127,8 @@ public Filter springSecurityFilterChain() throws Exception { } /** - * 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 @@ -143,19 +142,20 @@ public WebInvocationPrivilegeEvaluator privilegeEvaluator() { * instances used to create the web configuration. * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a * {@link WebSecurity} instance - * @param webSecurityConfigurers the + * @param beanFactory the bean factory to use to retrieve the relevant * {@code } instances used to * create the web configuration * @throws Exception */ @Autowired(required = false) public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor objectPostProcessor, - @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List> webSecurityConfigurers) - throws Exception { + ConfigurableListableBeanFactory beanFactory) throws Exception { this.webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor)); if (this.debugEnabled != null) { this.webSecurity.debug(this.debugEnabled); } + List> webSecurityConfigurers = new AutowiredWebSecurityConfigurersIgnoreParents( + beanFactory).getWebSecurityConfigurers(); webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; @@ -189,12 +189,6 @@ public static BeanFactoryPostProcessor conversionServicePostProcessor() { return new RsaKeyConversionServicePostProcessor(); } - @Bean - public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents( - ConfigurableListableBeanFactory beanFactory) { - return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory); - } - @Override public void setImportMetadata(AnnotationMetadata importMetadata) { Map enableWebSecurityAttrMap = importMetadata diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index 1c76a0bc841..78ff7f14e8c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -89,8 +89,12 @@ * * @author Rob Winch * @see EnableWebSecurity + * @deprecated Use a {@link org.springframework.security.web.SecurityFilterChain} Bean to + * configure {@link HttpSecurity} or a {@link WebSecurityCustomizer} Bean to configure + * {@link WebSecurity} */ @Order(100) +@Deprecated public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer { private final Log logger = LogFactory.getLog(WebSecurityConfigurerAdapter.class); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index 0837de2a7f8..441c0a84942 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java @@ -38,6 +38,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; @@ -146,6 +147,11 @@ public T loginProcessingUrl(String loginProcessingUrl) { return getSelf(); } + public T securityContextRepository(SecurityContextRepository securityContextRepository) { + this.authFilter.setSecurityContextRepository(securityContextRepository); + return getSelf(); + } + /** * Create the {@link RequestMatcher} given a loginProcessingUrl * @param loginProcessingUrl creates the {@link RequestMatcher} based upon the @@ -287,6 +293,12 @@ public void configure(B http) throws Exception { if (rememberMeServices != null) { this.authFilter.setRememberMeServices(rememberMeServices); } + SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class); + if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) { + SecurityContextRepository securityContextRepository = securityContextConfigurer + .getSecurityContextRepository(); + this.authFilter.setSecurityContextRepository(securityContextRepository); + } F filter = postProcess(this.authFilter); http.addFilter(filter); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 44d2416cd58..8b3a65e85d7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -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,14 +18,16 @@ import java.util.List; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -34,6 +36,7 @@ import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; 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; /** @@ -46,14 +49,25 @@ public final class AuthorizeHttpRequestsConfigurer> extends AbstractHttpConfigurer, H> { + static final AuthorizationManager permitAllAuthorizationManager = (a, + o) -> new AuthorizationDecision(true); + private final AuthorizationManagerRequestMatcherRegistry registry; + private final AuthorizationEventPublisher publisher; + /** * Creates an instance. * @param context the {@link ApplicationContext} to use */ public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { this.registry = new AuthorizationManagerRequestMatcherRegistry(context); + if (context.getBeanNamesForType(AuthorizationEventPublisher.class).length > 0) { + this.publisher = context.getBean(AuthorizationEventPublisher.class); + } + else { + this.publisher = new SpringAuthorizationEventPublisher(context); + } } /** @@ -70,6 +84,8 @@ public AuthorizationManagerRequestMatcherRegistry getRegistry() { public void configure(H http) { AuthorizationManager authorizationManager = this.registry.createAuthorizationManager(); AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + authorizationFilter.setAuthorizationEventPublisher(this.publisher); + authorizationFilter.setShouldFilterAllDispatcherTypes(this.registry.shouldFilterAllDispatcherTypes); http.addFilter(postProcess(authorizationFilter)); } @@ -81,6 +97,12 @@ private AuthorizationManagerRequestMatcherRegistry addMapping(List manager) { + this.registry.addFirst(matcher, manager); + return this.registry; + } + /** * Registry for mapping a {@link RequestMatcher} to an {@link AuthorizationManager}. * @@ -96,6 +118,8 @@ public final class AuthorizationManagerRequestMatcherRegistry private int mappingCount; + private boolean shouldFilterAllDispatcherTypes = false; + private AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) { setApplicationContext(context); } @@ -106,6 +130,12 @@ private void addMapping(RequestMatcher matcher, AuthorizationManager manager) { + this.unmappedMatchers = null; + this.managerBuilder.mappings((m) -> m.add(0, new RequestMatcherEntry<>(matcher, manager))); + this.mappingCount++; + } + private AuthorizationManager createAuthorizationManager() { Assert.state(this.unmappedMatchers == null, () -> "An incomplete mapping was found for " + this.unmappedMatchers @@ -143,6 +173,19 @@ public AuthorizationManagerRequestMatcherRegistry withObjectPostProcessor( return this; } + /** + * Sets whether all dispatcher types should be filtered. + * @param shouldFilter should filter all dispatcher types. Default is + * {@code false} + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + * @since 5.7 + */ + public AuthorizationManagerRequestMatcherRegistry shouldFilterAllDispatcherTypes(boolean shouldFilter) { + this.shouldFilterAllDispatcherTypes = shouldFilter; + return this; + } + /** * Return the {@link HttpSecurityBuilder} when done using the * {@link AuthorizeHttpRequestsConfigurer}. This is useful for method chaining. @@ -209,7 +252,7 @@ protected List getMatchers() { * customizations */ public AuthorizationManagerRequestMatcherRegistry permitAll() { - return access((a, o) -> new AuthorizationDecision(true)); + return access(permitAllAuthorizationManager); } /** @@ -221,6 +264,24 @@ public AuthorizationManagerRequestMatcherRegistry denyAll() { return access((a, o) -> new AuthorizationDecision(false)); } + public AuthorizationManagerRequestMatcherRegistry rememberMe() { + AuthorizationManager manager = AuthenticatedAuthorizationManager.rememberMe(); + return access(manager); + + } + + public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() { + AuthorizationManager manager = AuthenticatedAuthorizationManager + .fullyAuthenticated(); + return access(manager); + } + + public AuthorizationManagerRequestMatcherRegistry anonymous() { + AuthorizationManager manager = AuthenticatedAuthorizationManager.anonymous(); + return access(manager); + + } + /** * Specifies a user requires a role. * @param role the role that should be required which is prepended with ROLE_ diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index 19fc8ae0ca8..979084ce8c9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,9 @@ import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.PortMapper; +import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.access.channel.ChannelProcessor; @@ -75,6 +77,7 @@ * * @param the type of {@link HttpSecurityBuilder} that is being configured * @author Rob Winch + * @author Onur Kagan Ozcan * @since 3.2 */ public final class ChannelSecurityConfigurer> @@ -86,6 +89,8 @@ public final class ChannelSecurityConfigurer> private List channelProcessors; + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private final ChannelRequestMatcherRegistry REGISTRY; /** @@ -123,9 +128,11 @@ private List getChannelProcessors(H http) { if (portMapper != null) { RetryWithHttpEntryPoint httpEntryPoint = new RetryWithHttpEntryPoint(); httpEntryPoint.setPortMapper(portMapper); + httpEntryPoint.setRedirectStrategy(this.redirectStrategy); insecureChannelProcessor.setEntryPoint(httpEntryPoint); RetryWithHttpsEntryPoint httpsEntryPoint = new RetryWithHttpsEntryPoint(); httpsEntryPoint.setPortMapper(portMapper); + httpsEntryPoint.setRedirectStrategy(this.redirectStrategy); secureChannelProcessor.setEntryPoint(httpsEntryPoint); } insecureChannelProcessor = postProcess(insecureChannelProcessor); @@ -185,6 +192,17 @@ public ChannelRequestMatcherRegistry channelProcessors(List ch return this; } + /** + * Sets the {@link RedirectStrategy} instances to use in + * {@link RetryWithHttpEntryPoint} and {@link RetryWithHttpsEntryPoint} + * @param redirectStrategy + * @return the {@link ChannelSecurityConfigurer} for further customizations + */ + public ChannelRequestMatcherRegistry redirectStrategy(RedirectStrategy redirectStrategy) { + ChannelSecurityConfigurer.this.redirectStrategy = redirectStrategy; + return this; + } + /** * Return the {@link SecurityBuilder} when done using the * {@link SecurityConfigurer}. This is useful for method chaining. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index c0a3cd62ee6..b9c5cc7c633 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -237,8 +237,8 @@ private RequestMatcher getRequireCsrfProtectionMatcher() { /** * Gets the default {@link AccessDeniedHandler} from the - * {@link ExceptionHandlingConfigurer#getAccessDeniedHandler()} or create a - * {@link AccessDeniedHandlerImpl} if not available. + * {@link ExceptionHandlingConfigurer#getAccessDeniedHandler(HttpSecurityBuilder)} or + * create a {@link AccessDeniedHandlerImpl} if not available. * @param http the {@link HttpSecurityBuilder} * @return the {@link AccessDeniedHandler} */ @@ -247,7 +247,7 @@ private AccessDeniedHandler getDefaultAccessDeniedHandler(H http) { ExceptionHandlingConfigurer exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class); AccessDeniedHandler handler = null; if (exceptionConfig != null) { - handler = exceptionConfig.getAccessDeniedHandler(); + handler = exceptionConfig.getAccessDeniedHandler(http); } if (handler == null) { handler = new AccessDeniedHandlerImpl(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java index 1c0499693f8..0a990fa876f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java @@ -369,9 +369,7 @@ public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) { } /** - * Specify that URLs requires a specific IP Address or subnet. + * Specify that URLs requires a specific IP Address or subnet. * @param ipaddressExpression the ipaddress (i.e. 192.168.1.79) or local subnet * (i.e. 192.168.0/24) * @return the {@link ExpressionUrlAuthorizationConfigurer} for further diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index cd177581b91..bd20c509536 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -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. @@ -31,6 +31,9 @@ import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter; import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; @@ -97,6 +100,12 @@ public class HeadersConfigurer> private final PermissionsPolicyConfig permissionsPolicy = new PermissionsPolicyConfig(); + private final CrossOriginOpenerPolicyConfig crossOriginOpenerPolicy = new CrossOriginOpenerPolicyConfig(); + + private final CrossOriginEmbedderPolicyConfig crossOriginEmbedderPolicy = new CrossOriginEmbedderPolicyConfig(); + + private final CrossOriginResourcePolicyConfig crossOriginResourcePolicy = new CrossOriginResourcePolicyConfig(); + /** * Creates a new instance * @@ -392,6 +401,9 @@ private List getHeaderWriters() { addIfNotNull(writers, this.referrerPolicy.writer); addIfNotNull(writers, this.featurePolicy.writer); addIfNotNull(writers, this.permissionsPolicy.writer); + addIfNotNull(writers, this.crossOriginOpenerPolicy.writer); + addIfNotNull(writers, this.crossOriginEmbedderPolicy.writer); + addIfNotNull(writers, this.crossOriginResourcePolicy.writer); writers.addAll(this.headerWriters); return writers; } @@ -544,6 +556,129 @@ public PermissionsPolicyConfig permissionsPolicy(Customizer + * Cross-Origin-Opener-Policy header. + *

    + * Configuration is provided to the {@link CrossOriginOpenerPolicyHeaderWriter} which + * responsible for writing the header. + *

    + * @return the {@link CrossOriginOpenerPolicyConfig} for additional confniguration + * @since 5.7 + * @see CrossOriginOpenerPolicyHeaderWriter + */ + public CrossOriginOpenerPolicyConfig crossOriginOpenerPolicy() { + this.crossOriginOpenerPolicy.writer = new CrossOriginOpenerPolicyHeaderWriter(); + return this.crossOriginOpenerPolicy; + } + + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + *

    + * Calling this method automatically enables (includes) the + * {@code Cross-Origin-Opener-Policy} header in the response using the supplied + * policy. + *

    + *

    + * Configuration is provided to the {@link CrossOriginOpenerPolicyHeaderWriter} which + * responsible for writing the header. + *

    + * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginOpenerPolicyHeaderWriter + */ + public HeadersConfigurer crossOriginOpenerPolicy( + Customizer crossOriginOpenerPolicyCustomizer) { + this.crossOriginOpenerPolicy.writer = new CrossOriginOpenerPolicyHeaderWriter(); + crossOriginOpenerPolicyCustomizer.customize(this.crossOriginOpenerPolicy); + return HeadersConfigurer.this; + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + *

    + * Configuration is provided to the {@link CrossOriginEmbedderPolicyHeaderWriter} + * which is responsible for writing the header. + *

    + * @return the {@link CrossOriginEmbedderPolicyConfig} for additional customizations + * @since 5.7 + * @see CrossOriginEmbedderPolicyHeaderWriter + */ + public CrossOriginEmbedderPolicyConfig crossOriginEmbedderPolicy() { + this.crossOriginEmbedderPolicy.writer = new CrossOriginEmbedderPolicyHeaderWriter(); + return this.crossOriginEmbedderPolicy; + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + *

    + * Calling this method automatically enables (includes) the + * {@code Cross-Origin-Embedder-Policy} header in the response using the supplied + * policy. + *

    + *

    + * Configuration is provided to the {@link CrossOriginEmbedderPolicyHeaderWriter} + * which is responsible for writing the header. + *

    + * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginEmbedderPolicyHeaderWriter + */ + public HeadersConfigurer crossOriginEmbedderPolicy( + Customizer crossOriginEmbedderPolicyCustomizer) { + this.crossOriginEmbedderPolicy.writer = new CrossOriginEmbedderPolicyHeaderWriter(); + crossOriginEmbedderPolicyCustomizer.customize(this.crossOriginEmbedderPolicy); + return HeadersConfigurer.this; + } + + /** + * Allows configuration for + * Cross-Origin-Resource-Policy header. + *

    + * Configuration is provided to the {@link CrossOriginResourcePolicyHeaderWriter} + * which is responsible for writing the header: + *

    + * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginResourcePolicyHeaderWriter + */ + public CrossOriginResourcePolicyConfig crossOriginResourcePolicy() { + this.crossOriginResourcePolicy.writer = new CrossOriginResourcePolicyHeaderWriter(); + return this.crossOriginResourcePolicy; + } + + /** + * Allows configuration for + * Cross-Origin-Resource-Policy header. + *

    + * Calling this method automatically enables (includes) the + * {@code Cross-Origin-Resource-Policy} header in the response using the supplied + * policy. + *

    + *

    + * Configuration is provided to the {@link CrossOriginResourcePolicyHeaderWriter} + * which is responsible for writing the header: + *

    + * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginResourcePolicyHeaderWriter + */ + public HeadersConfigurer crossOriginResourcePolicy( + Customizer crossOriginResourcePolicyCustomizer) { + this.crossOriginResourcePolicy.writer = new CrossOriginResourcePolicyHeaderWriter(); + crossOriginResourcePolicyCustomizer.customize(this.crossOriginResourcePolicy); + return HeadersConfigurer.this; + } + public final class ContentTypeOptionsConfig { private XContentTypeOptionsHeaderWriter writer; @@ -1142,4 +1277,96 @@ public HeadersConfigurer and() { } + public final class CrossOriginOpenerPolicyConfig { + + private CrossOriginOpenerPolicyHeaderWriter writer; + + public CrossOriginOpenerPolicyConfig() { + } + + /** + * Sets the policy to be used in the {@code Cross-Origin-Opener-Policy} header + * @param openerPolicy a {@code Cross-Origin-Opener-Policy} + * @return the {@link CrossOriginOpenerPolicyConfig} for additional configuration + * @throws IllegalArgumentException if openerPolicy is null + */ + public CrossOriginOpenerPolicyConfig policy( + CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy openerPolicy) { + this.writer.setPolicy(openerPolicy); + return this; + } + + /** + * Allows completing configuration of Cross Origin Opener Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + + public final class CrossOriginEmbedderPolicyConfig { + + private CrossOriginEmbedderPolicyHeaderWriter writer; + + public CrossOriginEmbedderPolicyConfig() { + } + + /** + * Sets the policy to be used in the {@code Cross-Origin-Embedder-Policy} header + * @param embedderPolicy a {@code Cross-Origin-Embedder-Policy} + * @return the {@link CrossOriginEmbedderPolicyConfig} for additional + * configuration + * @throws IllegalArgumentException if embedderPolicy is null + */ + public CrossOriginEmbedderPolicyConfig policy( + CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy embedderPolicy) { + this.writer.setPolicy(embedderPolicy); + return this; + } + + /** + * Allows completing configuration of Cross-Origin-Embedder-Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + + public final class CrossOriginResourcePolicyConfig { + + private CrossOriginResourcePolicyHeaderWriter writer; + + public CrossOriginResourcePolicyConfig() { + } + + /** + * Sets the policy to be used in the {@code Cross-Origin-Resource-Policy} header + * @param resourcePolicy a {@code Cross-Origin-Resource-Policy} + * @return the {@link CrossOriginResourcePolicyConfig} for additional + * configuration + * @throws IllegalArgumentException if resourcePolicy is null + */ + public CrossOriginResourcePolicyConfig policy( + CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy resourcePolicy) { + this.writer.setPolicy(resourcePolicy); + return this; + } + + /** + * Allows completing configuration of Cross-Origin-Resource-Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java index 3af0eba1720..ac96e480109 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,11 +48,22 @@ static void permitAll(HttpSecurityBuilder> http RequestMatcher... requestMatchers) { ExpressionUrlAuthorizationConfigurer configurer = http .getConfigurer(ExpressionUrlAuthorizationConfigurer.class); - Assert.state(configurer != null, "permitAll only works with HttpSecurity.authorizeRequests()"); + AuthorizeHttpRequestsConfigurer httpConfigurer = http.getConfigurer(AuthorizeHttpRequestsConfigurer.class); + + boolean oneConfigurerPresent = configurer == null ^ httpConfigurer == null; + Assert.state(oneConfigurerPresent, + "permitAll only works with either HttpSecurity.authorizeRequests() or HttpSecurity.authorizeHttpRequests(). " + + "Please define one or the other but not both."); + for (RequestMatcher matcher : requestMatchers) { if (matcher != null) { - configurer.getRegistry().addMapping(0, new UrlMapping(matcher, - SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); + if (configurer != null) { + configurer.getRegistry().addMapping(0, new UrlMapping(matcher, + SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); + } + else { + httpConfigurer.addFirst(matcher, AuthorizeHttpRequestsConfigurer.permitAllAuthorizationManager); + } } } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java index d6d352995d5..24f57580e19 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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,6 +18,8 @@ import java.util.UUID; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.RememberMeAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -151,8 +153,10 @@ public RememberMeConfigurer useSecureCookie(boolean useSecureCookie) { * when a remember me token is valid. The default is to use the * {@link UserDetailsService} found by invoking * {@link HttpSecurity#getSharedObject(Class)} which is set when using - * {@link WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder)}. - * Alternatively, one can populate {@link #rememberMeServices(RememberMeServices)}. + * {@link WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder)}. When + * using a {@link org.springframework.security.web.SecurityFilterChain} bean, the + * default is to look for a {@link UserDetailsService} bean. Alternatively, one can + * populate {@link #rememberMeServices(RememberMeServices)}. * @param userDetailsService the {@link UserDetailsService} to configure * @return the {@link RememberMeConfigurer} for further customization * @see AbstractRememberMeServices @@ -395,15 +399,16 @@ private AbstractRememberMeServices createPersistentRememberMeServices(H http, St } /** - * Gets the {@link UserDetailsService} to use. Either the explicitly configure - * {@link UserDetailsService} from {@link #userDetailsService(UserDetailsService)} or - * a shared object from {@link HttpSecurity#getSharedObject(Class)}. + * Gets the {@link UserDetailsService} to use. Either the explicitly configured + * {@link UserDetailsService} from {@link #userDetailsService(UserDetailsService)}, a + * shared object from {@link HttpSecurity#getSharedObject(Class)} or the + * {@link UserDetailsService} bean. * @param http {@link HttpSecurity} to get the shared {@link UserDetailsService} * @return the {@link UserDetailsService} to use */ private UserDetailsService getUserDetailsService(H http) { if (this.userDetailsService == null) { - this.userDetailsService = http.getSharedObject(UserDetailsService.class); + this.userDetailsService = getSharedOrBean(http, UserDetailsService.class); } Assert.state(this.userDetailsService != null, () -> "userDetailsService cannot be null. Invoke " + RememberMeConfigurer.class.getSimpleName() @@ -431,4 +436,25 @@ private String getKey() { return this.key; } + private C getSharedOrBean(H http, Class type) { + C shared = http.getSharedObject(type); + if (shared != null) { + return shared; + } + return getBeanOrNull(type); + } + + private T getBeanOrNull(Class type) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + try { + return context.getBean(type); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java index 2139961a00a..7d9f5553ddc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java @@ -22,8 +22,10 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.session.ForceEagerSessionCreationFilter; /** * Allows persisting and restoring of the {@link SecurityContext} found on the @@ -62,6 +64,8 @@ public final class SecurityContextConfigurer> extends AbstractHttpConfigurer, H> { + private boolean requireExplicitSave; + /** * Creates a new instance * @see HttpSecurity#securityContext() @@ -79,23 +83,46 @@ public SecurityContextConfigurer securityContextRepository(SecurityContextRep return this; } + public SecurityContextConfigurer requireExplicitSave(boolean requireExplicitSave) { + this.requireExplicitSave = requireExplicitSave; + return this; + } + + boolean isRequireExplicitSave() { + return this.requireExplicitSave; + } + + SecurityContextRepository getSecurityContextRepository() { + SecurityContextRepository securityContextRepository = getBuilder() + .getSharedObject(SecurityContextRepository.class); + if (securityContextRepository == null) { + securityContextRepository = new HttpSessionSecurityContextRepository(); + } + return securityContextRepository; + } + @Override @SuppressWarnings("unchecked") public void configure(H http) { - SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); - if (securityContextRepository == null) { - securityContextRepository = new HttpSessionSecurityContextRepository(); + SecurityContextRepository securityContextRepository = getSecurityContextRepository(); + if (this.requireExplicitSave) { + SecurityContextHolderFilter securityContextHolderFilter = postProcess( + new SecurityContextHolderFilter(securityContextRepository)); + http.addFilter(securityContextHolderFilter); } - SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter( - securityContextRepository); - SessionManagementConfigurer sessionManagement = http.getConfigurer(SessionManagementConfigurer.class); - SessionCreationPolicy sessionCreationPolicy = (sessionManagement != null) - ? sessionManagement.getSessionCreationPolicy() : null; - if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) { - securityContextFilter.setForceEagerSessionCreation(true); + else { + SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter( + securityContextRepository); + SessionManagementConfigurer sessionManagement = http.getConfigurer(SessionManagementConfigurer.class); + SessionCreationPolicy sessionCreationPolicy = (sessionManagement != null) + ? sessionManagement.getSessionCreationPolicy() : null; + if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) { + securityContextFilter.setForceEagerSessionCreation(true); + http.addFilter(postProcess(new ForceEagerSessionCreationFilter())); + } + securityContextFilter = postProcess(securityContextFilter); + http.addFilter(securityContextFilter); } - securityContextFilter = postProcess(securityContextFilter); - http.addFilter(securityContextFilter); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 86b9cc0275a..504a396738c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -52,6 +52,8 @@ import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; +import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.session.SessionInformationExpiredStrategy; import org.springframework.security.web.session.SessionManagementFilter; @@ -201,6 +203,12 @@ public SessionManagementConfigurer sessionAuthenticationFailureHandler( * {@link HttpServletResponse#encodeRedirectURL(String)} or * {@link HttpServletResponse#encodeURL(String)}, otherwise disallows HTTP sessions to * be included in the URL. This prevents leaking information to external domains. + *

    + * This is achieved by guarding {@link HttpServletResponse#encodeURL} and + * {@link HttpServletResponse#encodeRedirectURL} invocations. Any code that also + * overrides either of these two methods, like + * {@link org.springframework.web.servlet.resource.ResourceUrlEncodingFilter}, needs + * to come after the security filter chain or risk being skipped. * @param enableSessionUrlRewriting true if should allow the JSESSIONID to be * rewritten into the URLs, else false (default) * @return the {@link SessionManagementConfigurer} for further customization @@ -370,6 +378,12 @@ public void configure(H http) { concurrentSessionFilter = postProcess(concurrentSessionFilter); http.addFilter(concurrentSessionFilter); } + if (!this.enableSessionUrlRewriting) { + http.addFilter(new DisableEncodeUrlFilter()); + } + if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) { + http.addFilter(new ForceEagerSessionCreationFilter()); + } } private ConcurrentSessionFilter createConcurrencyFilter(H http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index 93e1b092506..30de7141b42 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,15 +18,19 @@ import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; @@ -141,7 +145,9 @@ public X509Configurer userDetailsService(UserDetailsService userDetailsServic /** * Specifies the {@link AuthenticationUserDetailsService} to use. If not specified, * the shared {@link UserDetailsService} will be used to create a - * {@link UserDetailsByNameServiceWrapper}. + * {@link UserDetailsByNameServiceWrapper}. If a {@link SecurityFilterChain} bean is + * used instead of the {@link WebSecurityConfigurerAdapter}, then the + * {@link UserDetailsService} bean will be used by default. * @param authenticationUserDetailsService the * {@link AuthenticationUserDetailsService} to use * @return the {@link X509Configurer} for further customizations @@ -200,9 +206,30 @@ private X509AuthenticationFilter getFilter(AuthenticationManager authenticationM private AuthenticationUserDetailsService getAuthenticationUserDetailsService( H http) { if (this.authenticationUserDetailsService == null) { - userDetailsService(http.getSharedObject(UserDetailsService.class)); + userDetailsService(getSharedOrBean(http, UserDetailsService.class)); } return this.authenticationUserDetailsService; } + private C getSharedOrBean(H http, Class type) { + C shared = http.getSharedObject(type); + if (shared != null) { + return shared; + } + return getBeanOrNull(type); + } + + private T getBeanOrNull(Class type) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + try { + return context.getBean(type); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 5bdb85f3eb1..be1707e3946 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -73,12 +74,14 @@ import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * An {@link AbstractHttpConfigurer} for OAuth 2.0 Login, which leverages the OAuth 2.0 @@ -503,14 +506,28 @@ private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLogin new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); + RequestMatcher formLoginNotEnabled = getFormLoginNotEnabledRequestMatcher(http); LinkedHashMap entryPoints = new LinkedHashMap<>(); - entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)), - new LoginUrlAuthenticationEntryPoint(providerLoginPage)); + entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher), + formLoginNotEnabled), new LoginUrlAuthenticationEntryPoint(providerLoginPage)); DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint()); return loginEntryPoint; } + private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) { + DefaultLoginPageGeneratingFilter defaultLoginPageGeneratingFilter = http + .getSharedObject(DefaultLoginPageGeneratingFilter.class); + Field formLoginEnabledField = (defaultLoginPageGeneratingFilter != null) + ? ReflectionUtils.findField(DefaultLoginPageGeneratingFilter.class, "formLoginEnabled") : null; + if (formLoginEnabledField != null) { + ReflectionUtils.makeAccessible(formLoginEnabledField); + return (request) -> Boolean.FALSE + .equals(ReflectionUtils.getField(formLoginEnabledField, defaultLoginPageGeneratingFilter)); + } + return AnyRequestMatcher.INSTANCE; + } + /** * Configuration options for the Authorization Server's Authorization Endpoint. */ diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 02a99fdb2a4..0f267915973 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -18,6 +18,9 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; @@ -25,6 +28,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; @@ -51,6 +55,9 @@ import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.DelegatingAccessDeniedHandler; +import org.springframework.security.web.csrf.CsrfException; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; @@ -153,12 +160,19 @@ public final class OAuth2ResourceServerConfigurer(createAccessDeniedHandlers()), new BearerTokenAccessDeniedHandler()); private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher(); + private static Map, AccessDeniedHandler> createAccessDeniedHandlers() { + Map, AccessDeniedHandler> handlers = new HashMap<>(); + handlers.put(CsrfException.class, new AccessDeniedHandlerImpl()); + return handlers; + } + public OAuth2ResourceServerConfigurer(ApplicationContext context) { Assert.notNull(context, "context cannot be null"); this.context = context; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index aa1ddb29af6..a4cfb815dc8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,8 +19,6 @@ import java.util.LinkedHashMap; import java.util.Map; -import javax.servlet.Filter; - import org.opensaml.core.Version; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -34,8 +32,6 @@ import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; -import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; -import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; @@ -50,12 +46,14 @@ import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -113,11 +111,15 @@ public final class Saml2LoginConfigurer> extends AbstractAuthenticationFilterConfigurer, Saml2WebSsoAuthenticationFilter> { + private static final String OPEN_SAML_4_VERSION = "4"; + private String loginPage; - private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + private String authenticationRequestUri = "/saml2/authenticate/{registrationId}"; - private AuthenticationRequestEndpointConfig authenticationRequestEndpoint = new AuthenticationRequestEndpointConfig(); + private Saml2AuthenticationRequestResolver authenticationRequestResolver; + + private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; @@ -176,6 +178,20 @@ public Saml2LoginConfigurer loginPage(String loginPage) { return this; } + /** + * Use this {@link Saml2AuthenticationRequestResolver} for generating SAML 2.0 + * Authentication Requests. + * @param authenticationRequestResolver + * @return the {@link Saml2LoginConfigurer} for further configuration + * @since 5.7 + */ + public Saml2LoginConfigurer authenticationRequestResolver( + Saml2AuthenticationRequestResolver authenticationRequestResolver) { + Assert.notNull(authenticationRequestResolver, "authenticationRequestResolver cannot be null"); + this.authenticationRequestResolver = authenticationRequestResolver; + return this; + } + /** * Specifies the URL to validate the credentials. If specified a custom URL, consider * specifying a custom {@link AuthenticationConverter} via @@ -200,7 +216,7 @@ protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingU /** * {@inheritDoc} - * + *

    * Initializes this filter chain for SAML 2 Login. The following actions are taken: *

      *
    • The WebSSO endpoint has CSRF disabled, typically {@code /login/saml2/sso}
    • @@ -226,8 +242,8 @@ public void init(B http) throws Exception { super.init(http); } else { - Map providerUrlMap = getIdentityProviderUrlMap( - this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository); + Map providerUrlMap = getIdentityProviderUrlMap(this.authenticationRequestUri, + this.relyingPartyRegistrationRepository); boolean singleProvider = providerUrlMap.size() == 1; if (singleProvider) { // Setup auto-redirect to provider login page @@ -247,14 +263,16 @@ public void init(B http) throws Exception { /** * {@inheritDoc} - * + *

      * During the {@code configure} phase, a * {@link Saml2WebSsoAuthenticationRequestFilter} is added to handle SAML 2.0 * AuthNRequest redirects */ @Override public void configure(B http) throws Exception { - http.addFilter(this.authenticationRequestEndpoint.build(http)); + Saml2WebSsoAuthenticationRequestFilter filter = getAuthenticationRequestFilter(http); + filter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); + http.addFilter(postProcess(filter)); super.configure(http); if (this.authenticationManager == null) { registerDefaultAuthenticationProvider(http); @@ -264,6 +282,11 @@ public void configure(B http) throws Exception { } } + private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(B http) { + RelyingPartyRegistrationRepository registrations = relyingPartyRegistrationRepository(http); + return new DefaultRelyingPartyRegistrationResolver(registrations); + } + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) { if (this.relyingPartyRegistrationRepository == null) { this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); @@ -276,6 +299,44 @@ private void setAuthenticationRequestRepository(B http, saml2WebSsoAuthenticationFilter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); } + private Saml2WebSsoAuthenticationRequestFilter getAuthenticationRequestFilter(B http) { + Saml2AuthenticationRequestResolver authenticationRequestResolver = getAuthenticationRequestResolver(http); + if (authenticationRequestResolver != null) { + return new Saml2WebSsoAuthenticationRequestFilter(authenticationRequestResolver); + } + return new Saml2WebSsoAuthenticationRequestFilter(getAuthenticationRequestContextResolver(http), + getAuthenticationRequestFactory(http)); + } + + private Saml2AuthenticationRequestResolver getAuthenticationRequestResolver(B http) { + if (this.authenticationRequestResolver != null) { + return this.authenticationRequestResolver; + } + return getBeanOrNull(http, Saml2AuthenticationRequestResolver.class); + } + + private Saml2AuthenticationRequestFactory getAuthenticationRequestFactory(B http) { + Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class); + if (resolver != null) { + return resolver; + } + if (version().startsWith("4")) { + return OpenSaml4LoginSupportFactory.getAuthenticationRequestFactory(); + } + return new OpenSamlAuthenticationRequestFactory(); + } + + private Saml2AuthenticationRequestContextResolver getAuthenticationRequestContextResolver(B http) { + Saml2AuthenticationRequestContextResolver resolver = getBeanOrNull(http, + Saml2AuthenticationRequestContextResolver.class); + if (resolver != null) { + return resolver; + } + RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( + this.relyingPartyRegistrationRepository); + return new DefaultSaml2AuthenticationRequestContextResolver(registrationResolver); + } + private AuthenticationConverter getAuthenticationConverter(B http) { if (this.authenticationConverter != null) { return this.authenticationConverter; @@ -292,18 +353,9 @@ private AuthenticationConverter getAuthenticationConverter(B http) { return authenticationConverterBean; } - private String version() { - String version = Version.getVersion(); - if (version != null) { - return version; - } - return Version.class.getModule().getDescriptor().version().map(Object::toString) - .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); - } - private void registerDefaultAuthenticationProvider(B http) { if (version().startsWith("4")) { - http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider())); + http.authenticationProvider(postProcess(OpenSaml4LoginSupportFactory.getAuthenticationProvider())); } else { http.authenticationProvider(postProcess(new OpenSamlAuthenticationProvider())); @@ -325,8 +377,8 @@ private void initDefaultLoginFilter(B http) { return; } loginPageGeneratingFilter.setSaml2LoginEnabled(true); - loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName(this.getIdentityProviderUrlMap( - this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository)); + loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName( + this.getIdentityProviderUrlMap(this.authenticationRequestUri, this.relyingPartyRegistrationRepository)); loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); } @@ -353,6 +405,19 @@ private Saml2AuthenticationRequestRepository return repository; } + private String version() { + String version = Version.getVersion(); + if (StringUtils.hasText(version)) { + return version; + } + boolean openSaml4ClassPresent = ClassUtils + .isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null); + if (openSaml4ClassPresent) { + return OPEN_SAML_4_VERSION; + } + throw new IllegalStateException("cannot determine OpenSAML version"); + } + private C getSharedOrBean(B http, Class clazz) { C shared = http.getSharedObject(clazz); if (shared != null) { @@ -380,44 +445,31 @@ private void setSharedObject(B http, Class clazz, C object) { } } - private final class AuthenticationRequestEndpointConfig { - - private String filterProcessingUrl = "/saml2/authenticate/{registrationId}"; - - private AuthenticationRequestEndpointConfig() { - } - - private Filter build(B http) { - Saml2AuthenticationRequestFactory authenticationRequestResolver = getResolver(http); - Saml2AuthenticationRequestContextResolver contextResolver = getContextResolver(http); - Saml2AuthenticationRequestRepository repository = getAuthenticationRequestRepository( - http); - Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(contextResolver, - authenticationRequestResolver); - filter.setAuthenticationRequestRepository(repository); - return postProcess(filter); - } + private static class OpenSaml4LoginSupportFactory { - private Saml2AuthenticationRequestFactory getResolver(B http) { - Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class); - if (resolver == null) { - if (version().startsWith("4")) { - return new OpenSaml4AuthenticationRequestFactory(); - } - return new OpenSamlAuthenticationRequestFactory(); + private static Saml2AuthenticationRequestFactory getAuthenticationRequestFactory() { + try { + Class authenticationRequestFactory = ClassUtils.forName( + "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory", + OpenSaml4LoginSupportFactory.class.getClassLoader()); + return (Saml2AuthenticationRequestFactory) authenticationRequestFactory.getDeclaredConstructor() + .newInstance(); + } + catch (ReflectiveOperationException ex) { + throw new IllegalStateException("Could not instantiate OpenSaml4AuthenticationRequestFactory", ex); } - return resolver; } - private Saml2AuthenticationRequestContextResolver getContextResolver(B http) { - Saml2AuthenticationRequestContextResolver resolver = getBeanOrNull(http, - Saml2AuthenticationRequestContextResolver.class); - if (resolver == null) { - RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver( - Saml2LoginConfigurer.this.relyingPartyRegistrationRepository); - return new DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver); + private static AuthenticationProvider getAuthenticationProvider() { + try { + Class authenticationProvider = ClassUtils.forName( + "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider", + OpenSaml4LoginSupportFactory.class.getClassLoader()); + return (AuthenticationProvider) authenticationProvider.getDeclaredConstructor().newInstance(); + } + catch (ReflectiveOperationException ex) { + throw new IllegalStateException("Could not instantiate OpenSaml4AuthenticationProvider", ex); } - return resolver; } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index 45bd549c01c..5a974231ae8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -47,8 +47,6 @@ import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; @@ -67,6 +65,8 @@ import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Adds SAML 2.0 logout support. @@ -113,6 +113,8 @@ public final class Saml2LogoutConfigurer> extends AbstractHttpConfigurer, H> { + private static final String OPEN_SAML_4_VERSION = "4"; + private ApplicationContext context; private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; @@ -148,7 +150,7 @@ public Saml2LogoutConfigurer(ApplicationContext context) { *

      * The Relying Party triggers logout by POSTing to the endpoint. The Asserting Party * triggers logout based on what is specified by - * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}. + * {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}. * @param logoutUrl the URL that will invoke logout * @return the {@link LogoutConfigurer} for further customizations * @see LogoutConfigurer#logoutUrl(String) @@ -304,6 +306,19 @@ private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver( return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver); } + private String version() { + String version = Version.getVersion(); + if (StringUtils.hasText(version)) { + return version; + } + boolean openSaml4ClassPresent = ClassUtils + .isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null); + if (openSaml4ClassPresent) { + return OPEN_SAML_4_VERSION; + } + throw new IllegalStateException("cannot determine OpenSAML version"); + } + private C getBeanOrNull(Class clazz) { if (this.context == null) { return null; @@ -314,15 +329,6 @@ private C getBeanOrNull(Class clazz) { return this.context.getBean(clazz); } - private String version() { - String version = Version.getVersion(); - if (version != null) { - return version; - } - return Version.class.getModule().getDescriptor().version().map(Object::toString) - .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); - } - /** * A configurer for SAML 2.0 LogoutRequest components */ @@ -344,7 +350,7 @@ public final class LogoutRequestConfigurer { * *

      * The Asserting Party should use whatever HTTP method specified in - * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}. + * {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}. * @param logoutUrl the URL that will receive the SAML 2.0 Logout Request * @return the {@link LogoutRequestConfigurer} for further customizations * @see Saml2LogoutConfigurer#logoutUrl(String) @@ -403,7 +409,7 @@ private Saml2LogoutRequestResolver logoutRequestResolver( return this.logoutRequestResolver; } if (version().startsWith("4")) { - return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver); + return OpenSaml4LogoutSupportFactory.getLogoutRequestResolver(relyingPartyRegistrationResolver); } return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver); } @@ -426,7 +432,7 @@ public final class LogoutResponseConfigurer { * *

      * The Asserting Party should use whatever HTTP method specified in - * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}. + * {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}. * @param logoutUrl the URL that will receive the SAML 2.0 Logout Response * @return the {@link LogoutResponseConfigurer} for further customizations * @see Saml2LogoutConfigurer#logoutUrl(String) @@ -471,13 +477,13 @@ private Saml2LogoutResponseValidator logoutResponseValidator() { private Saml2LogoutResponseResolver logoutResponseResolver( RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { - if (this.logoutResponseResolver == null) { - if (version().startsWith("4")) { - return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver); - } - return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); + if (this.logoutResponseResolver != null) { + return this.logoutResponseResolver; } - return this.logoutResponseResolver; + if (version().startsWith("4")) { + return OpenSaml4LogoutSupportFactory.getLogoutResponseResolver(relyingPartyRegistrationResolver); + } + return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); } } @@ -520,4 +526,38 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut } + private static class OpenSaml4LogoutSupportFactory { + + private static Saml2LogoutResponseResolver getLogoutResponseResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + try { + Class logoutResponseResolver = ClassUtils.forName( + "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver", + OpenSaml4LogoutSupportFactory.class.getClassLoader()); + return (Saml2LogoutResponseResolver) logoutResponseResolver + .getDeclaredConstructor(RelyingPartyRegistrationResolver.class) + .newInstance(relyingPartyRegistrationResolver); + } + catch (ReflectiveOperationException ex) { + throw new IllegalStateException("Could not instantiate OpenSaml4LogoutResponseResolver", ex); + } + } + + private static Saml2LogoutRequestResolver getLogoutRequestResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + try { + Class logoutRequestResolver = ClassUtils.forName( + "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver", + OpenSaml4LogoutSupportFactory.class.getClassLoader()); + return (Saml2LogoutRequestResolver) logoutRequestResolver + .getDeclaredConstructor(RelyingPartyRegistrationResolver.class) + .newInstance(relyingPartyRegistrationResolver); + } + catch (ReflectiveOperationException ex) { + throw new IllegalStateException("Could not instantiate OpenSaml4LogoutRequestResolver", ex); + } + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java index eee7e34f368..dda9e7bea80 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -28,6 +28,7 @@ import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer; import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler; import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource; import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; @@ -43,7 +44,9 @@ * * @author Rob Winch * @since 4.0 + * @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead */ +@Deprecated public class MessageSecurityMetadataSourceRegistry { private static final String permitAll = "permitAll"; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java index 5a56f256506..e72f9a5b4bf 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -22,6 +22,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; @@ -41,6 +42,7 @@ * This {@code Configuration} is imported by {@link EnableWebFluxSecurity} * * @author Rob Winch + * @author Alavudin Kuttikkattil * @since 5.1 */ final class ReactiveOAuth2ClientImportSelector implements ImportSelector { @@ -64,14 +66,12 @@ static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigur private ReactiveOAuth2AuthorizedClientService authorizedClientService; + private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) { - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder - .builder().authorizationCode().refreshToken().clientCredentials().password().build(); - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( - this.clientRegistrationRepository, getAuthorizedClientRepository()); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager(); + if (authorizedClientManager != null) { configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager)); } } @@ -93,6 +93,13 @@ void setAuthorizedClientService(List auth } } + @Autowired(required = false) + void setAuthorizedClientManager(List authorizedClientManager) { + if (authorizedClientManager.size() == 1) { + this.authorizedClientManager = authorizedClientManager.get(0); + } + } + private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() { if (this.authorizedClientRepository != null) { return this.authorizedClientRepository; @@ -103,6 +110,23 @@ private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() { return null; } + private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() { + if (this.authorizedClientManager != null) { + return this.authorizedClientManager; + } + ReactiveOAuth2AuthorizedClientManager authorizedClientManager = null; + if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) { + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder + .builder().authorizationCode().refreshToken().clientCredentials().password().build(); + DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( + this.clientRegistrationRepository, getAuthorizedClientRepository()); + defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + authorizedClientManager = defaultReactiveOAuth2AuthorizedClientManager; + } + + return authorizedClientManager; + } + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java index 60139cd59aa..4d2dc99ede7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -81,9 +81,12 @@ * * @author Rob Winch * @since 4.0 + * @see WebSocketMessageBrokerSecurityConfiguration + * @deprecated Use {@link EnableWebSocketSecurity} instead */ @Order(Ordered.HIGHEST_PRECEDENCE + 100) @Import(ObjectPostProcessorConfiguration.class) +@Deprecated public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer implements SmartInitializingSingleton { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java new file mode 100644 index 00000000000..e80671aa50c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Allows configuring WebSocket Authorization. + * + *

      + * For example: + *

      + * + *
      + * @Configuration
      + * @EnableWebSocketSecurity
      + * public class WebSocketSecurityConfig {
      + *
      + * 	@Bean
      + * 	AuthorizationManager<Message<?>> (MessageMatcherDelegatingAuthorizationManager.Builder messages) {
      + * 		messages.simpDestMatchers("/user/queue/errors").permitAll()
      + * 				.simpDestMatchers("/admin/**").hasRole("ADMIN")
      + * 				.anyMessage().authenticated();
      + *		return messages.build();
      + * 	}
      + * }
      + * 
      + * + * @author Josh Cummings + * @since 5.8 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import(WebSocketMessageBrokerSecurityConfiguration.class) +public @interface EnableWebSocketSecurity { + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/MessageMatcherAuthorizationManagerConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/MessageMatcherAuthorizationManagerConfiguration.java new file mode 100644 index 00000000000..930ce773539 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/MessageMatcherAuthorizationManagerConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; +import org.springframework.util.AntPathMatcher; + +final class MessageMatcherAuthorizationManagerConfiguration { + + @Bean + @Scope("prototype") + MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder( + ApplicationContext context) { + return MessageMatcherDelegatingAuthorizationManager.builder().simpDestPathMatcher( + () -> (context.getBeanNamesForType(SimpAnnotationMethodMessageHandler.class).length > 0) + ? context.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher() + : new AntPathMatcher()); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java new file mode 100644 index 00000000000..8c5db34fb5a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.SpringAuthorizationEventPublisher; +import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; +import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver; +import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; +import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor; +import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor; +import org.springframework.util.Assert; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.server.HandshakeInterceptor; +import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler; +import org.springframework.web.socket.sockjs.SockJsService; +import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler; +import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService; + +@Order(Ordered.HIGHEST_PRECEDENCE + 100) +@Import(MessageMatcherAuthorizationManagerConfiguration.class) +final class WebSocketMessageBrokerSecurityConfiguration + implements WebSocketMessageBrokerConfigurer, SmartInitializingSingleton { + + private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping"; + + private MessageMatcherDelegatingAuthorizationManager b; + + private static final AuthorizationManager> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager + .builder().anyMessage().authenticated().build(); + + private final ChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor(); + + private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor(); + + private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor( + ANY_MESSAGE_AUTHENTICATED); + + private ApplicationContext context; + + WebSocketMessageBrokerSecurityConfiguration(ApplicationContext context) { + this.context = context; + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + this.authorizationChannelInterceptor + .setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context)); + registration.interceptors(this.securityContextChannelInterceptor, this.csrfChannelInterceptor, + this.authorizationChannelInterceptor); + } + + @Autowired(required = false) + void setAuthorizationManager(AuthorizationManager> authorizationManager) { + this.authorizationChannelInterceptor = new AuthorizationChannelInterceptor(authorizationManager); + } + + @Override + public void afterSingletonsInstantiated() { + SimpleUrlHandlerMapping mapping = getBeanOrNull(SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME, + SimpleUrlHandlerMapping.class); + if (mapping == null) { + return; + } + configureCsrf(mapping); + } + + private T getBeanOrNull(String name, Class type) { + Map beans = this.context.getBeansOfType(type); + return beans.get(name); + } + + private void configureCsrf(SimpleUrlHandlerMapping mapping) { + Map mappings = mapping.getHandlerMap(); + for (Object object : mappings.values()) { + if (object instanceof SockJsHttpRequestHandler) { + setHandshakeInterceptors((SockJsHttpRequestHandler) object); + } + else if (object instanceof WebSocketHttpRequestHandler) { + setHandshakeInterceptors((WebSocketHttpRequestHandler) object); + } + else { + throw new IllegalStateException( + "Bean " + SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME + " is expected to contain mappings to either a " + + "SockJsHttpRequestHandler or a WebSocketHttpRequestHandler but got " + object); + } + } + } + + private void setHandshakeInterceptors(SockJsHttpRequestHandler handler) { + SockJsService sockJsService = handler.getSockJsService(); + Assert.state(sockJsService instanceof TransportHandlingSockJsService, + () -> "sockJsService must be instance of TransportHandlingSockJsService got " + sockJsService); + TransportHandlingSockJsService transportHandlingSockJsService = (TransportHandlingSockJsService) sockJsService; + List handshakeInterceptors = transportHandlingSockJsService.getHandshakeInterceptors(); + List interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1); + interceptorsToSet.add(new CsrfTokenHandshakeInterceptor()); + interceptorsToSet.addAll(handshakeInterceptors); + transportHandlingSockJsService.setHandshakeInterceptors(interceptorsToSet); + } + + private void setHandshakeInterceptors(WebSocketHttpRequestHandler handler) { + List handshakeInterceptors = handler.getHandshakeInterceptors(); + List interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1); + interceptorsToSet.add(new CsrfTokenHandshakeInterceptor()); + interceptorsToSet.addAll(handshakeInterceptors); + handler.setHandshakeInterceptors(interceptorsToSet); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java index a4d79280ac8..33bdcb923b5 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java @@ -102,7 +102,9 @@ public BeanDefinition parse(Element element, ParserContext pc) { pc.getRegistry().registerAlias(id, alias); pc.getReaderContext().fireAliasRegistered(id, alias, pc.extractSource(element)); } - if (!BeanIds.AUTHENTICATION_MANAGER.equals(id)) { + if (!BeanIds.AUTHENTICATION_MANAGER.equals(id) + && !pc.getRegistry().containsBeanDefinition(BeanIds.AUTHENTICATION_MANAGER) + && !pc.getRegistry().isAlias(BeanIds.AUTHENTICATION_MANAGER)) { pc.getRegistry().registerAlias(id, BeanIds.AUTHENTICATION_MANAGER); pc.getReaderContext().fireAliasRegistered(id, BeanIds.AUTHENTICATION_MANAGER, pc.extractSource(element)); } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index d3c0ce32f4b..1150200f051 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -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. @@ -164,6 +164,8 @@ final class AuthenticationConfigBuilder { @SuppressWarnings("rawtypes") private ManagedList logoutHandlers; + private BeanMetadataElement logoutSuccessHandler; + private BeanDefinition loginPageGenerationFilter; private BeanDefinition logoutPageGenerationFilter; @@ -202,6 +204,20 @@ final class AuthenticationConfigBuilder { private BeanDefinition oauth2LoginLinks; + private BeanDefinition saml2AuthenticationUrlToProviderName; + + private BeanDefinition saml2AuthorizationRequestFilter; + + private String saml2AuthenticationFilterId; + + private String saml2AuthenticationRequestFilterId; + + private String saml2LogoutFilterId; + + private String saml2LogoutRequestFilterId; + + private String saml2LogoutResponseFilterId; + private boolean oauth2ClientEnabled; private BeanDefinition authorizationRequestRedirectFilter; @@ -220,8 +236,8 @@ final class AuthenticationConfigBuilder { AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc, SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager, - BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver, - BeanMetadataElement csrfLogoutHandler) { + BeanReference authenticationFilterSecurityContextRepositoryRef, BeanReference sessionStrategy, + BeanReference portMapper, BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) { this.httpElt = element; this.pc = pc; this.requestCache = requestCache; @@ -235,12 +251,16 @@ final class AuthenticationConfigBuilder { createRememberMeFilter(authenticationManager); createBasicFilter(authenticationManager); createBearerTokenAuthenticationFilter(authenticationManager); - createFormLoginFilter(sessionStrategy, authenticationManager); - createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager); - createOpenIDLoginFilter(sessionStrategy, authenticationManager); + createFormLoginFilter(sessionStrategy, authenticationManager, authenticationFilterSecurityContextRepositoryRef); + createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager, + authenticationFilterSecurityContextRepositoryRef); + createOpenIDLoginFilter(sessionStrategy, authenticationManager, + authenticationFilterSecurityContextRepositoryRef); + createSaml2LoginFilter(authenticationManager, authenticationFilterSecurityContextRepositoryRef); createX509Filter(authenticationManager); createJeeFilter(authenticationManager); createLogoutFilter(); + createSaml2LogoutFilter(); createLoginPageFilterIfNeeded(); createUserDetailsServiceFactory(); createExceptionTranslationFilter(); @@ -272,7 +292,8 @@ private void createRememberMeProvider(String key) { this.rememberMeProviderRef = new RuntimeBeanReference(id); } - void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authManager) { + void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authManager, + BeanReference authenticationFilterSecurityContextRepositoryRef) { Element formLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.FORM_LOGIN); RootBeanDefinition formFilter = null; if (formLoginElt != null || this.autoConfig) { @@ -288,6 +309,10 @@ void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authMana if (formFilter != null) { formFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation); formFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager); + if (authenticationFilterSecurityContextRepositoryRef != null) { + formFilter.getPropertyValues().addPropertyValue("securityContextRepository", + authenticationFilterSecurityContextRepositoryRef); + } // Id is required by login page filter this.formFilterId = this.pc.getReaderContext().generateBeanName(formFilter); this.pc.registerBeanComponent(new BeanComponentDefinition(formFilter, this.formFilterId)); @@ -296,13 +321,15 @@ void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authMana } void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache, - BeanReference authenticationManager) { - createOAuth2LoginFilter(sessionStrategy, authenticationManager); - createOAuth2ClientFilter(requestCache, authenticationManager); + BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef) { + createOAuth2LoginFilter(sessionStrategy, authenticationManager, + authenticationFilterSecurityContextRepositoryRef); + createOAuth2ClientFilter(requestCache, authenticationManager, authenticationFilterSecurityContextRepositoryRef); registerOAuth2ClientPostProcessors(); } - void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) { + void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager, + BeanReference authenticationFilterSecurityContextRepositoryRef) { Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN); if (oauth2LoginElt == null) { return; @@ -314,6 +341,10 @@ void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authMa BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository(); registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository); oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager); + if (authenticationFilterSecurityContextRepositoryRef != null) { + oauth2LoginFilterBean.getPropertyValues().addPropertyValue("securityContextRepository", + authenticationFilterSecurityContextRepositoryRef); + } // retrieve the other bean result BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider(); @@ -343,14 +374,15 @@ void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authMa this.oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginOidcAuthProviderId); } - void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager) { + void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager, + BeanReference authenticationFilterSecurityContextRepositoryRef) { Element oauth2ClientElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_CLIENT); if (oauth2ClientElt == null) { return; } this.oauth2ClientEnabled = true; OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser(requestCache, - authenticationManager); + authenticationManager, authenticationFilterSecurityContextRepositoryRef); parser.parse(oauth2ClientElt, this.pc); BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository(); registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository); @@ -395,7 +427,8 @@ private void registerOAuth2ClientPostProcessors() { } } - void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager) { + void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager, + BeanReference authenticationFilterSecurityContextRepositoryRef) { Element openIDLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OPENID_LOGIN); RootBeanDefinition openIDFilter = null; if (openIDLoginElt != null) { @@ -404,6 +437,10 @@ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authMa if (openIDFilter != null) { openIDFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation); openIDFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager); + if (authenticationFilterSecurityContextRepositoryRef != null) { + openIDFilter.getPropertyValues().addPropertyValue("securityContextRepository", + authenticationFilterSecurityContextRepositoryRef); + } // Required by login page filter this.openIDFilterId = this.pc.getReaderContext().generateBeanName(openIDFilter); this.pc.registerBeanComponent(new BeanComponentDefinition(openIDFilter, this.openIDFilterId)); @@ -412,6 +449,31 @@ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authMa } } + private void createSaml2LoginFilter(BeanReference authenticationManager, + BeanReference authenticationFilterSecurityContextRepositoryRef) { + Element saml2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGIN); + if (saml2LoginElt == null) { + return; + } + Saml2LoginBeanDefinitionParser parser = new Saml2LoginBeanDefinitionParser(this.csrfIgnoreRequestMatchers, + this.portMapper, this.portResolver, this.requestCache, this.allowSessionCreation, authenticationManager, + authenticationFilterSecurityContextRepositoryRef, this.authenticationProviders, + this.defaultEntryPointMappings); + BeanDefinition saml2WebSsoAuthenticationFilter = parser.parse(saml2LoginElt, this.pc); + this.saml2AuthorizationRequestFilter = parser.getSaml2WebSsoAuthenticationRequestFilter(); + + this.saml2AuthenticationFilterId = this.pc.getReaderContext().generateBeanName(saml2WebSsoAuthenticationFilter); + this.saml2AuthenticationRequestFilterId = this.pc.getReaderContext() + .generateBeanName(this.saml2AuthorizationRequestFilter); + this.saml2AuthenticationUrlToProviderName = parser.getSaml2AuthenticationUrlToProviderName(); + + // register the component + this.pc.registerBeanComponent( + new BeanComponentDefinition(saml2WebSsoAuthenticationFilter, this.saml2AuthenticationFilterId)); + this.pc.registerBeanComponent(new BeanComponentDefinition(this.saml2AuthorizationRequestFilter, + this.saml2AuthenticationRequestFilterId)); + } + /** * Parses OpenID 1.0 and 2.0 - related parts of configuration xmls * @param sessionStrategy sessionStrategy @@ -666,6 +728,12 @@ void createLoginPageFilterIfNeeded() { loginPageFilter.addPropertyValue("Oauth2LoginEnabled", true); loginPageFilter.addPropertyValue("Oauth2AuthenticationUrlToClientName", this.oauth2LoginLinks); } + if (this.saml2AuthenticationFilterId != null) { + loginPageFilter.addConstructorArgReference(this.saml2AuthenticationFilterId); + loginPageFilter.addPropertyValue("saml2LoginEnabled", true); + loginPageFilter.addPropertyValue("saml2AuthenticationUrlToProviderName", + this.saml2AuthenticationUrlToProviderName); + } this.loginPageGenerationFilter = loginPageFilter.getBeanDefinition(); this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition(); } @@ -682,7 +750,31 @@ void createLogoutFilter() { this.rememberMeServicesId, this.csrfLogoutHandler); this.logoutFilter = logoutParser.parse(logoutElt, this.pc); this.logoutHandlers = logoutParser.getLogoutHandlers(); + this.logoutSuccessHandler = logoutParser.getLogoutSuccessHandler(); + } + } + + private void createSaml2LogoutFilter() { + Element saml2LogoutElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGOUT); + if (saml2LogoutElt == null) { + return; } + Saml2LogoutBeanDefinitionParser parser = new Saml2LogoutBeanDefinitionParser(this.logoutHandlers, + this.logoutSuccessHandler); + parser.parse(saml2LogoutElt, this.pc); + BeanDefinition saml2LogoutFilter = parser.getLogoutFilter(); + BeanDefinition saml2LogoutRequestFilter = parser.getLogoutRequestFilter(); + BeanDefinition saml2LogoutResponseFilter = parser.getLogoutResponseFilter(); + this.saml2LogoutFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutFilter); + this.saml2LogoutRequestFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutRequestFilter); + this.saml2LogoutResponseFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutResponseFilter); + + // register the component + this.pc.registerBeanComponent(new BeanComponentDefinition(saml2LogoutFilter, this.saml2LogoutFilterId)); + this.pc.registerBeanComponent( + new BeanComponentDefinition(saml2LogoutRequestFilter, this.saml2LogoutRequestFilterId)); + this.pc.registerBeanComponent( + new BeanComponentDefinition(saml2LogoutResponseFilter, this.saml2LogoutResponseFilterId)); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -840,7 +932,8 @@ private BeanMetadataElement selectEntryPoint() { if (formLoginElt != null && this.oauth2LoginEntryPoint != null) { return this.formEntryPoint; } - // If form login was enabled through auto-config, and Oauth2 login was not + // If form login was enabled through auto-config, and Oauth2 login & Saml2 + // login was not // enabled then use form login if (this.oauth2LoginEntryPoint == null) { return this.formEntryPoint; @@ -923,6 +1016,20 @@ List getFilters() { filters.add(new OrderDecorator(this.authorizationCodeGrantFilter, SecurityFilters.OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER)); } + if (this.saml2AuthenticationFilterId != null) { + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationFilterId), + SecurityFilters.SAML2_AUTHENTICATION_FILTER)); + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationRequestFilterId), + SecurityFilters.SAML2_AUTHENTICATION_REQUEST_FILTER)); + } + if (this.saml2LogoutFilterId != null) { + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutFilterId), + SecurityFilters.SAML2_LOGOUT_FILTER)); + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutRequestFilterId), + SecurityFilters.SAML2_LOGOUT_REQUEST_FILTER)); + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutResponseFilterId), + SecurityFilters.SAML2_LOGOUT_RESPONSE_FILTER)); + } filters.add(new OrderDecorator(this.etf, SecurityFilters.EXCEPTION_TRANSLATION_FILTER)); return filters; } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java new file mode 100644 index 00000000000..e3ac6c915ae --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.Elements; +import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler; +import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +class AuthorizationFilterParser implements BeanDefinitionParser { + + private static final String ATT_USE_EXPRESSIONS = "use-expressions"; + + private static final String ATT_HTTP_METHOD = "method"; + + private static final String ATT_PATTERN = "pattern"; + + private static final String ATT_ACCESS = "access"; + + private static final String ATT_SERVLET_PATH = "servlet-path"; + + private String authorizationManagerRef; + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + if (!isUseExpressions(element)) { + parserContext.getReaderContext().error("AuthorizationManager must be used with `use-expressions=\"true\"", + element); + return null; + } + this.authorizationManagerRef = createAuthorizationManager(element, parserContext); + BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.rootBeanDefinition(AuthorizationFilter.class); + filterBuilder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + BeanDefinition filter = filterBuilder.addConstructorArgReference(this.authorizationManagerRef) + .getBeanDefinition(); + String id = element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE); + if (StringUtils.hasText(id)) { + parserContext.registerComponent(new BeanComponentDefinition(filter, id)); + parserContext.getRegistry().registerBeanDefinition(id, filter); + } + return filter; + } + + String getAuthorizationManagerRef() { + return this.authorizationManagerRef; + } + + private String createAuthorizationManager(Element element, ParserContext parserContext) { + XmlReaderContext context = parserContext.getReaderContext(); + String authorizationManagerRef = element.getAttribute("authorization-manager-ref"); + if (StringUtils.hasText(authorizationManagerRef)) { + return authorizationManagerRef; + } + Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER); + String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null; + if (expressionHandlerRef == null) { + expressionHandlerRef = registerDefaultExpressionHandler(parserContext); + } + MatcherType matcherType = MatcherType.fromElement(element); + ManagedMap matcherToExpression = new ManagedMap<>(); + List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_URL); + for (Element interceptMessage : interceptMessages) { + String accessExpression = interceptMessage.getAttribute(ATT_ACCESS); + BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder + .rootBeanDefinition(WebExpressionAuthorizationManager.class); + authorizationManager.addPropertyReference("expressionHandler", expressionHandlerRef); + authorizationManager.addConstructorArgValue(accessExpression); + BeanMetadataElement matcher = createMatcher(matcherType, interceptMessage, parserContext); + matcherToExpression.put(matcher, authorizationManager.getBeanDefinition()); + } + BeanDefinitionBuilder mds = BeanDefinitionBuilder + .rootBeanDefinition(RequestMatcherDelegatingAuthorizationManagerFactory.class); + mds.setFactoryMethod("createRequestMatcherDelegatingAuthorizationManager"); + mds.addConstructorArgValue(matcherToExpression); + return context.registerWithGeneratedName(mds.getBeanDefinition()); + } + + private BeanMetadataElement createMatcher(MatcherType matcherType, Element urlElt, ParserContext parserContext) { + String path = urlElt.getAttribute(ATT_PATTERN); + String matcherRef = urlElt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUEST_MATCHER_REF); + boolean hasMatcherRef = StringUtils.hasText(matcherRef); + if (!hasMatcherRef && !StringUtils.hasText(path)) { + parserContext.getReaderContext().error("path attribute cannot be empty or null", urlElt); + } + String method = urlElt.getAttribute(ATT_HTTP_METHOD); + if (!StringUtils.hasText(method)) { + method = null; + } + String servletPath = urlElt.getAttribute(ATT_SERVLET_PATH); + if (!StringUtils.hasText(servletPath)) { + servletPath = null; + } + else if (!MatcherType.mvc.equals(matcherType)) { + parserContext.getReaderContext().error( + ATT_SERVLET_PATH + " is not applicable for request-matcher: '" + matcherType.name() + "'", urlElt); + } + return hasMatcherRef ? new RuntimeBeanReference(matcherRef) + : matcherType.createMatcher(parserContext, path, method, servletPath); + } + + String registerDefaultExpressionHandler(ParserContext pc) { + BeanDefinition expressionHandler = GrantedAuthorityDefaultsParserUtils.registerWithDefaultRolePrefix(pc, + DefaultWebSecurityExpressionHandlerBeanFactory.class); + String expressionHandlerRef = pc.getReaderContext().generateBeanName(expressionHandler); + pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler, expressionHandlerRef)); + return expressionHandlerRef; + } + + boolean isUseExpressions(Element elt) { + String useExpressions = elt.getAttribute(ATT_USE_EXPRESSIONS); + return !StringUtils.hasText(useExpressions) || "true".equals(useExpressions); + } + + private static class RequestMatcherDelegatingAuthorizationManagerFactory { + + private static AuthorizationManager createRequestMatcherDelegatingAuthorizationManager( + Map> beans) { + RequestMatcherDelegatingAuthorizationManager.Builder builder = RequestMatcherDelegatingAuthorizationManager + .builder(); + for (Map.Entry> entry : beans + .entrySet()) { + builder.add(entry.getKey(), entry.getValue()); + } + return builder.add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated()).build(); + } + + } + + static class DefaultWebSecurityExpressionHandlerBeanFactory + extends GrantedAuthorityDefaultsParserUtils.AbstractGrantedAuthorityDefaultsBeanFactory { + + private DefaultHttpSecurityExpressionHandler handler = new DefaultHttpSecurityExpressionHandler(); + + @Override + public DefaultHttpSecurityExpressionHandler getBean() { + this.handler.setDefaultRolePrefix(this.rolePrefix); + return this.handler; + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java index 7f42ff724e2..b980f635a73 100644 --- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -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. @@ -36,6 +36,9 @@ import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter; import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; @@ -122,6 +125,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String PERMISSIONS_POLICY_ELEMENT = "permissions-policy"; + private static final String CROSS_ORIGIN_OPENER_POLICY_ELEMENT = "cross-origin-opener-policy"; + + private static final String CROSS_ORIGIN_EMBEDDER_POLICY_ELEMENT = "cross-origin-embedder-policy"; + + private static final String CROSS_ORIGIN_RESOURCE_POLICY_ELEMENT = "cross-origin-resource-policy"; + private static final String ALLOW_FROM = "ALLOW-FROM"; private ManagedList headerWriters; @@ -144,6 +153,9 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { parseReferrerPolicyElement(element, parserContext); parseFeaturePolicyElement(element, parserContext); parsePermissionsPolicyElement(element, parserContext); + parseCrossOriginOpenerPolicy(disabled, element); + parseCrossOriginEmbedderPolicy(disabled, element); + parseCrossOriginResourcePolicy(disabled, element); parseHeaderElements(element); boolean noWriters = this.headerWriters.isEmpty(); if (disabled && !noWriters) { @@ -376,6 +388,75 @@ private void addPermissionsPolicy(Element permissionsPolicyElement, ParserContex this.headerWriters.add(headersWriter.getBeanDefinition()); } + private void parseCrossOriginOpenerPolicy(boolean elementDisabled, Element element) { + if (elementDisabled || element == null) { + return; + } + CrossOriginOpenerPolicyHeaderWriter writer = new CrossOriginOpenerPolicyHeaderWriter(); + Element crossOriginOpenerPolicyElement = DomUtils.getChildElementByTagName(element, + CROSS_ORIGIN_OPENER_POLICY_ELEMENT); + if (crossOriginOpenerPolicyElement != null) { + addCrossOriginOpenerPolicy(crossOriginOpenerPolicyElement, writer); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(CrossOriginOpenerPolicyHeaderWriter.class, () -> writer); + this.headerWriters.add(builder.getBeanDefinition()); + } + + private void parseCrossOriginEmbedderPolicy(boolean elementDisabled, Element element) { + if (elementDisabled || element == null) { + return; + } + CrossOriginEmbedderPolicyHeaderWriter writer = new CrossOriginEmbedderPolicyHeaderWriter(); + Element crossOriginEmbedderPolicyElement = DomUtils.getChildElementByTagName(element, + CROSS_ORIGIN_EMBEDDER_POLICY_ELEMENT); + if (crossOriginEmbedderPolicyElement != null) { + addCrossOriginEmbedderPolicy(crossOriginEmbedderPolicyElement, writer); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(CrossOriginEmbedderPolicyHeaderWriter.class, () -> writer); + this.headerWriters.add(builder.getBeanDefinition()); + } + + private void parseCrossOriginResourcePolicy(boolean elementDisabled, Element element) { + if (elementDisabled || element == null) { + return; + } + CrossOriginResourcePolicyHeaderWriter writer = new CrossOriginResourcePolicyHeaderWriter(); + Element crossOriginResourcePolicyElement = DomUtils.getChildElementByTagName(element, + CROSS_ORIGIN_RESOURCE_POLICY_ELEMENT); + if (crossOriginResourcePolicyElement != null) { + addCrossOriginResourcePolicy(crossOriginResourcePolicyElement, writer); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(CrossOriginResourcePolicyHeaderWriter.class, () -> writer); + this.headerWriters.add(builder.getBeanDefinition()); + } + + private void addCrossOriginResourcePolicy(Element crossOriginResourcePolicyElement, + CrossOriginResourcePolicyHeaderWriter writer) { + String policy = crossOriginResourcePolicyElement.getAttribute(ATT_POLICY); + if (StringUtils.hasText(policy)) { + writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.from(policy)); + } + } + + private void addCrossOriginEmbedderPolicy(Element crossOriginEmbedderPolicyElement, + CrossOriginEmbedderPolicyHeaderWriter writer) { + String policy = crossOriginEmbedderPolicyElement.getAttribute(ATT_POLICY); + if (StringUtils.hasText(policy)) { + writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.from(policy)); + } + } + + private void addCrossOriginOpenerPolicy(Element crossOriginOpenerPolicyElement, + CrossOriginOpenerPolicyHeaderWriter writer) { + String policy = crossOriginOpenerPolicyElement.getAttribute(ATT_POLICY); + if (StringUtils.hasText(policy)) { + writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.from(policy)); + } + } + private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) { context.getReaderContext().error("Only one of '" + attrName + "' or '" + otherAttrName + "' can be set.", element); diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index e3339b3b136..e7539558b56 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -41,6 +41,7 @@ import org.springframework.security.config.Elements; import org.springframework.security.config.http.GrantedAuthorityDefaultsParserUtils.AbstractGrantedAuthorityDefaultsBeanFactory; import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; import org.springframework.security.web.access.channel.ChannelProcessingFilter; @@ -59,6 +60,7 @@ import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; @@ -67,6 +69,8 @@ import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; +import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy; @@ -104,10 +108,16 @@ class HttpConfigurationBuilder { private static final String ATT_SECURITY_CONTEXT_REPOSITORY = "security-context-repository-ref"; + private static final String ATT_SECURITY_CONTEXT_EXPLICIT_SAVE = "security-context-explicit-save"; + private static final String ATT_INVALID_SESSION_STRATEGY_REF = "invalid-session-strategy-ref"; private static final String ATT_DISABLE_URL_REWRITING = "disable-url-rewriting"; + private static final String ATT_USE_AUTHORIZATION_MGR = "use-authorization-manager"; + + private static final String ATT_AUTHORIZATION_MGR = "authorization-manager-ref"; + private static final String ATT_ACCESS_MGR = "access-decision-manager-ref"; private static final String ATT_ONCE_PER_REQUEST = "once-per-request"; @@ -144,6 +154,8 @@ class HttpConfigurationBuilder { private BeanDefinition securityContextPersistenceFilter; + private BeanDefinition forceEagerSessionCreationFilter; + private BeanReference contextRepoRef; private BeanReference sessionRegistryRef; @@ -176,6 +188,8 @@ class HttpConfigurationBuilder { private BeanDefinition csrfFilter; + private BeanDefinition disableUrlRewriteFilter; + private BeanDefinition wellKnownChangePasswordRedirectFilter; private BeanMetadataElement csrfLogoutHandler; @@ -201,15 +215,17 @@ class HttpConfigurationBuilder { String createSession = element.getAttribute(ATT_CREATE_SESSION); this.sessionPolicy = !StringUtils.hasText(createSession) ? SessionCreationPolicy.IF_REQUIRED : createPolicy(createSession); + createForceEagerSessionCreationFilter(); + createDisableEncodeUrlFilter(); createCsrfFilter(); - createSecurityContextPersistenceFilter(); + createSecurityPersistence(); createSessionManagementFilters(); createWebAsyncManagerFilter(); createRequestCacheFilter(); createServletApiFilter(authenticationManager); createJaasApiFilter(); createChannelProcessingFilter(); - createFilterSecurityInterceptor(authenticationManager); + createFilterSecurity(authenticationManager); createAddHeadersFilter(); createCorsFilter(); createWellKnownChangePasswordRedirectFilter(); @@ -278,19 +294,51 @@ static String createPath(String path, boolean lowerCase) { return lowerCase ? path.toLowerCase() : path; } + BeanReference getSecurityContextRepositoryForAuthenticationFilters() { + return (isExplicitSave()) ? this.contextRepoRef : null; + } + + private void createSecurityPersistence() { + createSecurityContextRepository(); + if (isExplicitSave()) { + createSecurityContextHolderFilter(); + } + else { + createSecurityContextPersistenceFilter(); + } + } + + private boolean isExplicitSave() { + String explicitSaveAttr = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_EXPLICIT_SAVE); + return Boolean.parseBoolean(explicitSaveAttr); + } + + private void createForceEagerSessionCreationFilter() { + if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) { + this.forceEagerSessionCreationFilter = new RootBeanDefinition(ForceEagerSessionCreationFilter.class); + } + } + private void createSecurityContextPersistenceFilter() { BeanDefinitionBuilder scpf = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextPersistenceFilter.class); - String repoRef = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_REPOSITORY); - String disableUrlRewriting = this.httpElt.getAttribute(ATT_DISABLE_URL_REWRITING); - if (!StringUtils.hasText(disableUrlRewriting)) { - disableUrlRewriting = "true"; - } - if (StringUtils.hasText(repoRef)) { - if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) { - scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE); - } + switch (this.sessionPolicy) { + case ALWAYS: + scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE); + break; + case NEVER: + scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE); + break; + default: + scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE); } - else { + scpf.addConstructorArgValue(this.contextRepoRef); + + this.securityContextPersistenceFilter = scpf.getBeanDefinition(); + } + + private void createSecurityContextRepository() { + String repoRef = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_REPOSITORY); + if (!StringUtils.hasText(repoRef)) { BeanDefinitionBuilder contextRepo; if (this.sessionPolicy == SessionCreationPolicy.STATELESS) { contextRepo = BeanDefinitionBuilder.rootBeanDefinition(NullSecurityContextRepository.class); @@ -300,17 +348,14 @@ private void createSecurityContextPersistenceFilter() { switch (this.sessionPolicy) { case ALWAYS: contextRepo.addPropertyValue("allowSessionCreation", Boolean.TRUE); - scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE); break; case NEVER: contextRepo.addPropertyValue("allowSessionCreation", Boolean.FALSE); - scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE); break; default: contextRepo.addPropertyValue("allowSessionCreation", Boolean.TRUE); - scpf.addPropertyValue("forceEagerSessionCreation", Boolean.FALSE); } - if ("true".equals(disableUrlRewriting)) { + if (isDisableUrlRewriting()) { contextRepo.addPropertyValue("disableUrlRewriting", Boolean.TRUE); } } @@ -320,9 +365,17 @@ private void createSecurityContextPersistenceFilter() { } this.contextRepoRef = new RuntimeBeanReference(repoRef); - scpf.addConstructorArgValue(this.contextRepoRef); + } - this.securityContextPersistenceFilter = scpf.getBeanDefinition(); + private boolean isDisableUrlRewriting() { + String disableUrlRewriting = this.httpElt.getAttribute(ATT_DISABLE_URL_REWRITING); + return !"false".equals(disableUrlRewriting); + } + + private void createSecurityContextHolderFilter() { + BeanDefinitionBuilder filter = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextHolderFilter.class); + filter.addConstructorArgValue(this.contextRepoRef); + this.securityContextPersistenceFilter = filter.getBeanDefinition(); } private void createSessionManagementFilters() { @@ -627,6 +680,35 @@ private void createRequestCacheFilter() { this.requestCacheAwareFilter.getConstructorArgumentValues().addGenericArgumentValue(this.requestCache); } + private void createFilterSecurity(BeanReference authManager) { + boolean useAuthorizationManager = Boolean.parseBoolean(this.httpElt.getAttribute(ATT_USE_AUTHORIZATION_MGR)); + if (useAuthorizationManager) { + createAuthorizationFilter(); + return; + } + if (StringUtils.hasText(this.httpElt.getAttribute(ATT_AUTHORIZATION_MGR))) { + createAuthorizationFilter(); + return; + } + createFilterSecurityInterceptor(authManager); + } + + private void createAuthorizationFilter() { + AuthorizationFilterParser authorizationFilterParser = new AuthorizationFilterParser(); + BeanDefinition fsiBean = authorizationFilterParser.parse(this.httpElt, this.pc); + String fsiId = this.pc.getReaderContext().generateBeanName(fsiBean); + this.pc.registerBeanComponent(new BeanComponentDefinition(fsiBean, fsiId)); + // Create and register a AuthorizationManagerWebInvocationPrivilegeEvaluator for + // use with + // taglibs etc. + BeanDefinition wipe = BeanDefinitionBuilder + .rootBeanDefinition(AuthorizationManagerWebInvocationPrivilegeEvaluator.class) + .addConstructorArgReference(authorizationFilterParser.getAuthorizationManagerRef()).getBeanDefinition(); + this.pc.registerBeanComponent( + new BeanComponentDefinition(wipe, this.pc.getReaderContext().generateBeanName(wipe))); + this.fsi = new RuntimeBeanReference(fsiId); + } + private void createFilterSecurityInterceptor(BeanReference authManager) { boolean useExpressions = FilterInvocationSecurityMetadataSourceParser.isUseExpressions(this.httpElt); RootBeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser @@ -685,6 +767,12 @@ private void createCorsFilter() { } + private void createDisableEncodeUrlFilter() { + if (isDisableUrlRewriting()) { + this.disableUrlRewriteFilter = new RootBeanDefinition(DisableEncodeUrlFilter.class); + } + } + private void createCsrfFilter() { Element elmt = DomUtils.getChildElementByTagName(this.httpElt, Elements.CSRF); this.csrfParser = new CsrfBeanDefinitionParser(); @@ -724,6 +812,13 @@ BeanReference getRequestCache() { List getFilters() { List filters = new ArrayList<>(); + if (this.forceEagerSessionCreationFilter != null) { + filters.add(new OrderDecorator(this.forceEagerSessionCreationFilter, + SecurityFilters.FORCE_EAGER_SESSION_FILTER)); + } + if (this.disableUrlRewriteFilter != null) { + filters.add(new OrderDecorator(this.disableUrlRewriteFilter, SecurityFilters.DISABLE_ENCODE_URL_FILTER)); + } if (this.cpf != null) { filters.add(new OrderDecorator(this.cpf, SecurityFilters.CHANNEL_FILTER)); } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 970245d1345..7d0be016ce8 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -144,9 +144,11 @@ private BeanReference createFilterChain(Element element, ParserContext pc) { boolean forceAutoConfig = isDefaultHttpConfig(element); HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, forceAutoConfig, pc, portMapper, portResolver, authenticationManager); + httpBldr.getSecurityContextRepositoryForAuthenticationFilters(); AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, forceAutoConfig, pc, httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager, - httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler()); + httpBldr.getSecurityContextRepositoryForAuthenticationFilters(), httpBldr.getSessionStrategy(), + portMapper, portResolver, httpBldr.getCsrfLogoutHandler()); httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers()); httpBldr.setEntryPoint(authBldr.getEntryPointBean()); httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean()); diff --git a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java index 65c1b3b931a..51d9462d5e0 100644 --- a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java @@ -59,6 +59,8 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { private boolean csrfEnabled; + private BeanMetadataElement logoutSuccessHandler; + LogoutBeanDefinitionParser(String loginPageUrl, String rememberMeServices, BeanMetadataElement csrfLogoutHandler) { this.defaultLogoutUrl = loginPageUrl + "?logout"; this.rememberMeServices = rememberMeServices; @@ -98,6 +100,7 @@ public BeanDefinition parse(Element element, ParserContext pc) { pc.extractSource(element)); } builder.addConstructorArgReference(successHandlerRef); + this.logoutSuccessHandler = new RuntimeBeanReference(successHandlerRef); } else { // Use the logout URL if no handler set @@ -137,4 +140,8 @@ ManagedList getLogoutHandlers() { return this.logoutHandlers; } + BeanMetadataElement getLogoutSuccessHandler() { + return this.logoutSuccessHandler; + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java index 3a72f62585d..f2c1ebd0f09 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java @@ -50,6 +50,8 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { private final BeanReference authenticationManager; + private final BeanReference authenticationFilterSecurityContextRepositoryRef; + private BeanDefinition defaultAuthorizedClientRepository; private BeanDefinition authorizationRequestRedirectFilter; @@ -58,9 +60,11 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { private BeanDefinition authorizationCodeAuthenticationProvider; - OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager) { + OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager, + BeanReference authenticationFilterSecurityContextRepositoryRef) { this.requestCache = requestCache; this.authenticationManager = authenticationManager; + this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef; } @Override @@ -92,11 +96,16 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { this.authorizationRequestRedirectFilter = authorizationRequestRedirectFilterBuilder .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository) .addPropertyValue("requestCache", this.requestCache).getBeanDefinition(); - this.authorizationCodeGrantFilter = BeanDefinitionBuilder + BeanDefinitionBuilder authorizationCodeGrantFilterBldr = BeanDefinitionBuilder .rootBeanDefinition(OAuth2AuthorizationCodeGrantFilter.class) .addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository) .addConstructorArgValue(this.authenticationManager) - .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository).getBeanDefinition(); + .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository); + if (this.authenticationFilterSecurityContextRepositoryRef != null) { + authorizationCodeGrantFilterBldr.addPropertyValue("securityContextRepository", + this.authenticationFilterSecurityContextRepositoryRef); + } + this.authorizationCodeGrantFilter = authorizationCodeGrantFilterBldr.getBeanDefinition(); BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(authorizationCodeGrantElt); this.authorizationCodeAuthenticationProvider = BeanDefinitionBuilder diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java new file mode 100644 index 00000000000..147166c471f --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java @@ -0,0 +1,323 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; +import org.springframework.security.config.Elements; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * SAML 2.0 Login {@link BeanDefinitionParser} + * + * @author Marcus da Coregio + * @since 5.7 + */ +final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser { + + private static final String DEFAULT_LOGIN_URI = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL; + + private static final String DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL = "/saml2/authenticate/{registrationId}"; + + private static final String ATT_LOGIN_PROCESSING_URL = "login-processing-url"; + + private static final String ATT_LOGIN_PAGE = "login-page"; + + private static final String ELT_RELYING_PARTY_REGISTRATION = "relying-party-registration"; + + private static final String ELT_REGISTRATION_ID = "registration-id"; + + private static final String ATT_AUTHENTICATION_FAILURE_HANDLER_REF = "authentication-failure-handler-ref"; + + private static final String ATT_AUTHENTICATION_SUCCESS_HANDLER_REF = "authentication-success-handler-ref"; + + private static final String ATT_AUTHENTICATION_MANAGER_REF = "authentication-manager-ref"; + + private final List csrfIgnoreRequestMatchers; + + private final BeanReference portMapper; + + private final BeanReference portResolver; + + private final BeanReference requestCache; + + private final boolean allowSessionCreation; + + private final BeanReference authenticationManager; + + private final BeanReference authenticationFilterSecurityContextRepositoryRef; + + private final List authenticationProviders; + + private final Map entryPoints; + + private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + + private BeanDefinition saml2WebSsoAuthenticationRequestFilter; + + private BeanDefinition saml2AuthenticationUrlToProviderName; + + Saml2LoginBeanDefinitionParser(List csrfIgnoreRequestMatchers, BeanReference portMapper, + BeanReference portResolver, BeanReference requestCache, boolean allowSessionCreation, + BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef, + List authenticationProviders, Map entryPoints) { + this.csrfIgnoreRequestMatchers = csrfIgnoreRequestMatchers; + this.portMapper = portMapper; + this.portResolver = portResolver; + this.requestCache = requestCache; + this.allowSessionCreation = allowSessionCreation; + this.authenticationManager = authenticationManager; + this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef; + this.authenticationProviders = authenticationProviders; + this.entryPoints = entryPoints; + } + + @Override + public BeanDefinition parse(Element element, ParserContext pc) { + String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL); + if (StringUtils.hasText(loginProcessingUrl)) { + this.loginProcessingUrl = loginProcessingUrl; + } + BeanDefinition saml2LoginBeanConfig = BeanDefinitionBuilder.rootBeanDefinition(Saml2LoginBeanConfig.class) + .getBeanDefinition(); + String saml2LoginBeanConfigId = pc.getReaderContext().generateBeanName(saml2LoginBeanConfig); + pc.registerBeanComponent(new BeanComponentDefinition(saml2LoginBeanConfig, saml2LoginBeanConfigId)); + registerDefaultCsrfOverride(); + BeanMetadataElement relyingPartyRegistrationRepository = Saml2LoginBeanDefinitionParserUtils + .getRelyingPartyRegistrationRepository(element); + BeanMetadataElement authenticationRequestRepository = Saml2LoginBeanDefinitionParserUtils + .getAuthenticationRequestRepository(element); + BeanMetadataElement authenticationRequestResolver = Saml2LoginBeanDefinitionParserUtils + .getAuthenticationRequestResolver(element); + if (authenticationRequestResolver == null) { + authenticationRequestResolver = Saml2LoginBeanDefinitionParserUtils + .createDefaultAuthenticationRequestResolver(relyingPartyRegistrationRepository); + } + BeanMetadataElement authenticationConverter = Saml2LoginBeanDefinitionParserUtils + .getAuthenticationConverter(element); + if (authenticationConverter == null) { + if (!this.loginProcessingUrl.contains("{registrationId}")) { + pc.getReaderContext().error("loginProcessingUrl must contain {registrationId} path variable", element); + } + authenticationConverter = Saml2LoginBeanDefinitionParserUtils + .createDefaultAuthenticationConverter(relyingPartyRegistrationRepository); + } + // Configure the Saml2WebSsoAuthenticationFilter + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder = BeanDefinitionBuilder + .rootBeanDefinition(Saml2WebSsoAuthenticationFilter.class) + .addConstructorArgValue(authenticationConverter).addConstructorArgValue(this.loginProcessingUrl) + .addPropertyValue("authenticationRequestRepository", authenticationRequestRepository); + resolveLoginPage(element, pc); + resolveAuthenticationSuccessHandler(element, saml2WebSsoAuthenticationFilterBuilder); + resolveAuthenticationFailureHandler(element, saml2WebSsoAuthenticationFilterBuilder); + resolveAuthenticationManager(element, saml2WebSsoAuthenticationFilterBuilder); + resolveSecurityContextRepository(element, saml2WebSsoAuthenticationFilterBuilder); + // Configure the Saml2WebSsoAuthenticationRequestFilter + this.saml2WebSsoAuthenticationRequestFilter = BeanDefinitionBuilder + .rootBeanDefinition(Saml2WebSsoAuthenticationRequestFilter.class) + .addConstructorArgValue(authenticationRequestResolver) + .addPropertyValue("authenticationRequestRepository", authenticationRequestRepository) + .getBeanDefinition(); + BeanDefinition saml2AuthenticationProvider = Saml2LoginBeanDefinitionParserUtils.createAuthenticationProvider(); + this.authenticationProviders.add( + new RuntimeBeanReference(pc.getReaderContext().registerWithGeneratedName(saml2AuthenticationProvider))); + this.saml2AuthenticationUrlToProviderName = BeanDefinitionBuilder.rootBeanDefinition(Map.class) + .setFactoryMethodOnBean("getAuthenticationUrlToProviderName", saml2LoginBeanConfigId) + .getBeanDefinition(); + return saml2WebSsoAuthenticationFilterBuilder.getBeanDefinition(); + } + + private void resolveAuthenticationManager(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + String authenticationManagerRef = element.getAttribute(ATT_AUTHENTICATION_MANAGER_REF); + if (StringUtils.hasText(authenticationManagerRef)) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationManager", + authenticationManagerRef); + } + else { + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationManager", + this.authenticationManager); + } + } + + private void resolveSecurityContextRepository(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + if (this.authenticationFilterSecurityContextRepositoryRef != null) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("securityContextRepository", + this.authenticationFilterSecurityContextRepositoryRef); + } + } + + private void resolveLoginPage(Element element, ParserContext parserContext) { + String loginPage = element.getAttribute(ATT_LOGIN_PAGE); + Object source = parserContext.extractSource(element); + BeanDefinition saml2LoginAuthenticationEntryPoint = null; + if (StringUtils.hasText(loginPage)) { + WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source); + saml2LoginAuthenticationEntryPoint = BeanDefinitionBuilder + .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginPage) + .addPropertyValue("portMapper", this.portMapper).addPropertyValue("portResolver", this.portResolver) + .getBeanDefinition(); + } + else { + Map identityProviderUrlMap = getIdentityProviderUrlMap(element); + if (identityProviderUrlMap.size() == 1) { + String loginUrl = identityProviderUrlMap.entrySet().iterator().next().getKey(); + saml2LoginAuthenticationEntryPoint = BeanDefinitionBuilder + .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginUrl) + .addPropertyValue("portMapper", this.portMapper) + .addPropertyValue("portResolver", this.portResolver).getBeanDefinition(); + } + } + if (saml2LoginAuthenticationEntryPoint != null) { + BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AntPathRequestMatcher.class); + requestMatcherBuilder.addConstructorArgValue(this.loginProcessingUrl); + BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition(); + this.entryPoints.put(requestMatcher, saml2LoginAuthenticationEntryPoint); + } + } + + private void resolveAuthenticationFailureHandler(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF); + if (StringUtils.hasText(authenticationFailureHandlerRef)) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler", + authenticationFailureHandlerRef); + } + else { + BeanDefinitionBuilder failureHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"); + failureHandlerBuilder.addConstructorArgValue( + DEFAULT_LOGIN_URI + "?" + DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME); + failureHandlerBuilder.addPropertyValue("allowSessionCreation", this.allowSessionCreation); + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationFailureHandler", + failureHandlerBuilder.getBeanDefinition()); + } + } + + private void resolveAuthenticationSuccessHandler(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF); + if (StringUtils.hasText(authenticationSuccessHandlerRef)) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler", + authenticationSuccessHandlerRef); + } + else { + BeanDefinitionBuilder successHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler") + .addPropertyValue("requestCache", this.requestCache); + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationSuccessHandler", + successHandlerBuilder.getBeanDefinition()); + } + } + + private void registerDefaultCsrfOverride() { + BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AntPathRequestMatcher.class); + requestMatcherBuilder.addConstructorArgValue(this.loginProcessingUrl); + BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition(); + this.csrfIgnoreRequestMatchers.add(requestMatcher); + } + + private Map getIdentityProviderUrlMap(Element element) { + Map idps = new LinkedHashMap<>(); + Element relyingPartyRegistrationsElt = DomUtils.getChildElementByTagName( + element.getOwnerDocument().getDocumentElement(), Elements.RELYING_PARTY_REGISTRATIONS); + String authenticationRequestProcessingUrl = DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL; + if (relyingPartyRegistrationsElt != null) { + List relyingPartyRegList = DomUtils.getChildElementsByTagName(relyingPartyRegistrationsElt, + ELT_RELYING_PARTY_REGISTRATION); + for (Element relyingPartyReg : relyingPartyRegList) { + String registrationId = relyingPartyReg.getAttribute(ELT_REGISTRATION_ID); + idps.put(authenticationRequestProcessingUrl.replace("{registrationId}", registrationId), + registrationId); + } + } + return idps; + } + + BeanDefinition getSaml2WebSsoAuthenticationRequestFilter() { + return this.saml2WebSsoAuthenticationRequestFilter; + } + + BeanDefinition getSaml2AuthenticationUrlToProviderName() { + return this.saml2AuthenticationUrlToProviderName; + } + + /** + * Wrapper bean class to provide configuration from applicationContext + */ + public static class Saml2LoginBeanConfig implements ApplicationContextAware { + + private ApplicationContext context; + + @SuppressWarnings({ "unchecked", "unused" }) + Map getAuthenticationUrlToProviderName() { + Iterable relyingPartyRegistrations = null; + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository = this.context + .getBean(RelyingPartyRegistrationRepository.class); + ResolvableType type = ResolvableType.forInstance(relyingPartyRegistrationRepository).as(Iterable.class); + if (type != ResolvableType.NONE + && RelyingPartyRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { + relyingPartyRegistrations = (Iterable) relyingPartyRegistrationRepository; + } + if (relyingPartyRegistrations == null) { + return Collections.emptyMap(); + } + String authenticationRequestProcessingUrl = DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL; + Map saml2AuthenticationUrlToProviderName = new HashMap<>(); + relyingPartyRegistrations.forEach((registration) -> saml2AuthenticationUrlToProviderName.put( + authenticationRequestProcessingUrl.replace("{registrationId}", registration.getRegistrationId()), + registration.getRegistrationId())); + return saml2AuthenticationUrlToProviderName; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.context = context; + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java new file mode 100644 index 00000000000..225bcbe0870 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; +import org.springframework.util.StringUtils; + +/** + * @author Marcus da Coregio + * @since 5.7 + */ +final class Saml2LoginBeanDefinitionParserUtils { + + private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref"; + + private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref"; + + private static final String ATT_AUTHENTICATION_REQUEST_RESOLVER_REF = "authentication-request-resolver-ref"; + + private static final String ATT_AUTHENTICATION_CONVERTER = "authentication-converter-ref"; + + private Saml2LoginBeanDefinitionParserUtils() { + } + + static BeanMetadataElement getRelyingPartyRegistrationRepository(Element element) { + String relyingPartyRegistrationRepositoryRef = element + .getAttribute(ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF); + if (StringUtils.hasText(relyingPartyRegistrationRepositoryRef)) { + return new RuntimeBeanReference(relyingPartyRegistrationRepositoryRef); + } + return new RuntimeBeanReference(RelyingPartyRegistrationRepository.class); + } + + static BeanMetadataElement getAuthenticationRequestRepository(Element element) { + String authenticationRequestRepositoryRef = element.getAttribute(ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF); + if (StringUtils.hasText(authenticationRequestRepositoryRef)) { + return new RuntimeBeanReference(authenticationRequestRepositoryRef); + } + return BeanDefinitionBuilder.rootBeanDefinition(HttpSessionSaml2AuthenticationRequestRepository.class) + .getBeanDefinition(); + } + + static BeanMetadataElement getAuthenticationRequestResolver(Element element) { + String authenticationRequestContextResolver = element.getAttribute(ATT_AUTHENTICATION_REQUEST_RESOLVER_REF); + if (StringUtils.hasText(authenticationRequestContextResolver)) { + return new RuntimeBeanReference(authenticationRequestContextResolver); + } + return null; + } + + static BeanMetadataElement createDefaultAuthenticationRequestResolver( + BeanMetadataElement relyingPartyRegistrationRepository) { + BeanMetadataElement defaultRelyingPartyRegistrationResolver = BeanDefinitionBuilder + .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class) + .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver") + .addConstructorArgValue(defaultRelyingPartyRegistrationResolver).getBeanDefinition(); + } + + static BeanDefinition createAuthenticationProvider() { + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider") + .getBeanDefinition(); + } + + static BeanMetadataElement getAuthenticationConverter(Element element) { + String authenticationConverter = element.getAttribute(ATT_AUTHENTICATION_CONVERTER); + if (StringUtils.hasText(authenticationConverter)) { + return new RuntimeBeanReference(authenticationConverter); + } + return null; + } + + static BeanDefinition createDefaultAuthenticationConverter(BeanMetadataElement relyingPartyRegistrationRepository) { + AbstractBeanDefinition resolver = BeanDefinitionBuilder + .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class) + .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition(Saml2AuthenticationTokenConverter.class) + .addConstructorArgValue(resolver).getBeanDefinition(); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java new file mode 100644 index 00000000000..cb3bc26355f --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +import javax.servlet.http.HttpServletRequest; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * SAML 2.0 Single Logout {@link BeanDefinitionParser} + * + * @author Marcus da Coregio + * @since 5.7 + */ +final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser { + + private static final String ATT_LOGOUT_REQUEST_URL = "logout-request-url"; + + private static final String ATT_LOGOUT_RESPONSE_URL = "logout-response-url"; + + private static final String ATT_LOGOUT_URL = "logout-url"; + + private List logoutHandlers; + + private String logoutUrl = "/logout"; + + private String logoutRequestUrl = "/logout/saml2/slo"; + + private String logoutResponseUrl = "/logout/saml2/slo"; + + private BeanMetadataElement logoutSuccessHandler; + + private BeanDefinition logoutRequestFilter; + + private BeanDefinition logoutResponseFilter; + + private BeanDefinition logoutFilter; + + Saml2LogoutBeanDefinitionParser(ManagedList logoutHandlers, + BeanMetadataElement logoutSuccessHandler) { + this.logoutHandlers = logoutHandlers; + this.logoutSuccessHandler = logoutSuccessHandler; + } + + @Override + public BeanDefinition parse(Element element, ParserContext pc) { + String logoutUrl = element.getAttribute(ATT_LOGOUT_URL); + if (StringUtils.hasText(logoutUrl)) { + this.logoutUrl = logoutUrl; + } + String logoutRequestUrl = element.getAttribute(ATT_LOGOUT_REQUEST_URL); + if (StringUtils.hasText(logoutRequestUrl)) { + this.logoutRequestUrl = logoutRequestUrl; + } + String logoutResponseUrl = element.getAttribute(ATT_LOGOUT_RESPONSE_URL); + if (StringUtils.hasText(logoutResponseUrl)) { + this.logoutResponseUrl = logoutResponseUrl; + } + WebConfigUtils.validateHttpRedirect(this.logoutUrl, pc, element); + WebConfigUtils.validateHttpRedirect(this.logoutRequestUrl, pc, element); + WebConfigUtils.validateHttpRedirect(this.logoutResponseUrl, pc, element); + if (CollectionUtils.isEmpty(this.logoutHandlers)) { + this.logoutHandlers = createDefaultLogoutHandlers(); + } + if (this.logoutSuccessHandler == null) { + this.logoutSuccessHandler = createDefaultLogoutSuccessHandler(); + } + BeanMetadataElement relyingPartyRegistrationRepository = Saml2LogoutBeanDefinitionParserUtils + .getRelyingPartyRegistrationRepository(element); + BeanMetadataElement registrations = BeanDefinitionBuilder + .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class) + .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition(); + BeanMetadataElement logoutResponseResolver = Saml2LogoutBeanDefinitionParserUtils + .getLogoutResponseResolver(element, registrations); + BeanMetadataElement logoutRequestValidator = Saml2LogoutBeanDefinitionParserUtils + .getLogoutRequestValidator(element); + BeanMetadataElement logoutRequestMatcher = createSaml2LogoutRequestMatcher(); + this.logoutRequestFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutRequestFilter.class) + .addConstructorArgValue(registrations).addConstructorArgValue(logoutRequestValidator) + .addConstructorArgValue(logoutResponseResolver).addConstructorArgValue(this.logoutHandlers) + .addPropertyValue("logoutRequestMatcher", logoutRequestMatcher).getBeanDefinition(); + BeanMetadataElement logoutResponseValidator = Saml2LogoutBeanDefinitionParserUtils + .getLogoutResponseValidator(element); + BeanMetadataElement logoutRequestRepository = Saml2LogoutBeanDefinitionParserUtils + .getLogoutRequestRepository(element); + BeanMetadataElement logoutResponseMatcher = createSaml2LogoutResponseMatcher(); + this.logoutResponseFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutResponseFilter.class) + .addConstructorArgValue(registrations).addConstructorArgValue(logoutResponseValidator) + .addConstructorArgValue(this.logoutSuccessHandler) + .addPropertyValue("logoutRequestMatcher", logoutResponseMatcher) + .addPropertyValue("logoutRequestRepository", logoutRequestRepository).getBeanDefinition(); + BeanMetadataElement logoutRequestResolver = Saml2LogoutBeanDefinitionParserUtils + .getLogoutRequestResolver(element, registrations); + BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder + .rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class) + .addConstructorArgValue(logoutRequestResolver).getBeanDefinition(); + this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class) + .addConstructorArgValue(saml2LogoutRequestSuccessHandler).addConstructorArgValue(this.logoutHandlers) + .addPropertyValue("logoutRequestMatcher", createLogoutRequestMatcher()).getBeanDefinition(); + return null; + } + + private static List createDefaultLogoutHandlers() { + List handlers = new ManagedList<>(); + handlers.add(BeanDefinitionBuilder.rootBeanDefinition(SecurityContextLogoutHandler.class).getBeanDefinition()); + handlers.add(BeanDefinitionBuilder.rootBeanDefinition(LogoutSuccessEventPublishingLogoutHandler.class) + .getBeanDefinition()); + return handlers; + } + + private static BeanMetadataElement createDefaultLogoutSuccessHandler() { + return BeanDefinitionBuilder.rootBeanDefinition(SimpleUrlLogoutSuccessHandler.class) + .addPropertyValue("defaultTargetUrl", "/login?logout").getBeanDefinition(); + } + + private BeanMetadataElement createLogoutRequestMatcher() { + BeanMetadataElement logoutMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class) + .addConstructorArgValue(this.logoutUrl).addConstructorArgValue("POST").getBeanDefinition(); + BeanMetadataElement saml2Matcher = BeanDefinitionBuilder.rootBeanDefinition(Saml2RequestMatcher.class) + .getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class) + .addConstructorArgValue(toManagedList(logoutMatcher, saml2Matcher)).getBeanDefinition(); + } + + private BeanMetadataElement createSaml2LogoutRequestMatcher() { + BeanMetadataElement logoutRequestMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class) + .addConstructorArgValue(this.logoutRequestUrl).getBeanDefinition(); + BeanMetadataElement saml2RequestMatcher = BeanDefinitionBuilder + .rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLRequest") + .getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class) + .addConstructorArgValue(toManagedList(logoutRequestMatcher, saml2RequestMatcher)).getBeanDefinition(); + } + + private BeanMetadataElement createSaml2LogoutResponseMatcher() { + BeanMetadataElement logoutResponseMatcher = BeanDefinitionBuilder + .rootBeanDefinition(AntPathRequestMatcher.class).addConstructorArgValue(this.logoutResponseUrl) + .getBeanDefinition(); + BeanMetadataElement saml2ResponseMatcher = BeanDefinitionBuilder + .rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLResponse") + .getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class) + .addConstructorArgValue(toManagedList(logoutResponseMatcher, saml2ResponseMatcher)).getBeanDefinition(); + } + + private static List toManagedList(BeanMetadataElement... elements) { + List managedList = new ManagedList<>(); + managedList.addAll(Arrays.asList(elements)); + return managedList; + } + + BeanDefinition getLogoutRequestFilter() { + return this.logoutRequestFilter; + } + + BeanDefinition getLogoutResponseFilter() { + return this.logoutResponseFilter; + } + + BeanDefinition getLogoutFilter() { + return this.logoutFilter; + } + + private static class ParameterRequestMatcher implements RequestMatcher { + + Predicate test = Objects::nonNull; + + String name; + + ParameterRequestMatcher(String name) { + this.name = name; + } + + @Override + public boolean matches(HttpServletRequest request) { + return this.test.test(request.getParameter(this.name)); + } + + } + + private static class Saml2RequestMatcher implements RequestMatcher { + + @Override + public boolean matches(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return false; + } + return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal; + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java new file mode 100644 index 00000000000..96ca597889d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; +import org.springframework.util.StringUtils; + +/** + * @author Marcus da Coregio + * @since 5.7 + */ +final class Saml2LogoutBeanDefinitionParserUtils { + + private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref"; + + private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref"; + + private static final String ATT_LOGOUT_REQUEST_REPOSITORY_REF = "logout-request-repository-ref"; + + private static final String ATT_LOGOUT_REQUEST_RESOLVER_REF = "logout-request-resolver-ref"; + + private static final String ATT_LOGOUT_RESPONSE_RESOLVER_REF = "logout-response-resolver-ref"; + + private static final String ATT_LOGOUT_RESPONSE_VALIDATOR_REF = "logout-response-validator-ref"; + + private Saml2LogoutBeanDefinitionParserUtils() { + } + + static BeanMetadataElement getRelyingPartyRegistrationRepository(Element element) { + String relyingPartyRegistrationRepositoryRef = element + .getAttribute(ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF); + if (StringUtils.hasText(relyingPartyRegistrationRepositoryRef)) { + return new RuntimeBeanReference(relyingPartyRegistrationRepositoryRef); + } + return new RuntimeBeanReference(RelyingPartyRegistrationRepository.class); + } + + static BeanMetadataElement getLogoutResponseResolver(Element element, BeanMetadataElement registrations) { + String logoutResponseResolver = element.getAttribute(ATT_LOGOUT_RESPONSE_RESOLVER_REF); + if (StringUtils.hasText(logoutResponseResolver)) { + return new RuntimeBeanReference(logoutResponseResolver); + } + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver") + .addConstructorArgValue(registrations).getBeanDefinition(); + } + + static BeanMetadataElement getLogoutRequestValidator(Element element) { + String logoutRequestValidator = element.getAttribute(ATT_LOGOUT_REQUEST_VALIDATOR_REF); + if (StringUtils.hasText(logoutRequestValidator)) { + return new RuntimeBeanReference(logoutRequestValidator); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutRequestValidator.class).getBeanDefinition(); + } + + static BeanMetadataElement getLogoutResponseValidator(Element element) { + String logoutResponseValidator = element.getAttribute(ATT_LOGOUT_RESPONSE_VALIDATOR_REF); + if (StringUtils.hasText(logoutResponseValidator)) { + return new RuntimeBeanReference(logoutResponseValidator); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutResponseValidator.class).getBeanDefinition(); + } + + static BeanMetadataElement getLogoutRequestRepository(Element element) { + String logoutRequestRepository = element.getAttribute(ATT_LOGOUT_REQUEST_REPOSITORY_REF); + if (StringUtils.hasText(logoutRequestRepository)) { + return new RuntimeBeanReference(logoutRequestRepository); + } + return BeanDefinitionBuilder.rootBeanDefinition(HttpSessionLogoutRequestRepository.class).getBeanDefinition(); + } + + static BeanMetadataElement getLogoutRequestResolver(Element element, BeanMetadataElement registrations) { + String logoutRequestResolver = element.getAttribute(ATT_LOGOUT_REQUEST_RESOLVER_REF); + if (StringUtils.hasText(logoutRequestResolver)) { + return new RuntimeBeanReference(logoutRequestResolver); + } + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver") + .addConstructorArgValue(registrations).getBeanDefinition(); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index c9b053a3b66..c13012d4a78 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -29,6 +29,10 @@ enum SecurityFilters { FIRST(Integer.MIN_VALUE), + DISABLE_ENCODE_URL_FILTER, + + FORCE_EAGER_SESSION_FILTER, + CHANNEL_FILTER, SECURITY_CONTEXT_FILTER, @@ -41,12 +45,20 @@ enum SecurityFilters { CORS_FILTER, + SAML2_LOGOUT_REQUEST_FILTER, + + SAML2_LOGOUT_RESPONSE_FILTER, + CSRF_FILTER, + SAML2_LOGOUT_FILTER, + LOGOUT_FILTER, OAUTH2_AUTHORIZATION_REQUEST_FILTER, + SAML2_AUTHENTICATION_REQUEST_FILTER, + X509_FILTER, PRE_AUTH_FILTER, @@ -55,6 +67,8 @@ enum SecurityFilters { OAUTH2_LOGIN_FILTER, + SAML2_AUTHENTICATION_FILTER, + FORM_LOGIN_FILTER, OPENID_FILTER, diff --git a/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java new file mode 100644 index 00000000000..16069f09a47 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public abstract class AbstractLdapAuthenticationManagerFactory { + + AbstractLdapAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + } + + private BaseLdapPathContextSource contextSource; + + private String[] userDnPatterns; + + private LdapAuthoritiesPopulator ldapAuthoritiesPopulator; + + private GrantedAuthoritiesMapper authoritiesMapper; + + private UserDetailsContextMapper userDetailsContextMapper; + + private String userSearchFilter; + + private String userSearchBase = ""; + + /** + * Sets the {@link BaseLdapPathContextSource} used to perform LDAP authentication. + * @param contextSource the {@link BaseLdapPathContextSource} used to perform LDAP + * authentication + */ + public void setContextSource(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + } + + /** + * Gets the {@link BaseLdapPathContextSource} used to perform LDAP authentication. + * @return the {@link BaseLdapPathContextSource} used to perform LDAP authentication + */ + protected final BaseLdapPathContextSource getContextSource() { + return this.contextSource; + } + + /** + * Sets the {@link LdapAuthoritiesPopulator} used to obtain a list of granted + * authorities for an LDAP user. + * @param ldapAuthoritiesPopulator the {@link LdapAuthoritiesPopulator} to use + */ + public void setLdapAuthoritiesPopulator(LdapAuthoritiesPopulator ldapAuthoritiesPopulator) { + this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator; + } + + /** + * Sets the {@link GrantedAuthoritiesMapper} used for converting the authorities + * loaded from storage to a new set of authorities which will be associated to the + * {@link UsernamePasswordAuthenticationToken}. + * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the + * user's authorities + */ + public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + this.authoritiesMapper = authoritiesMapper; + } + + /** + * Sets a custom strategy to be used for creating the {@link UserDetails} which will + * be stored as the principal in the {@link Authentication}. + * @param userDetailsContextMapper the strategy instance + */ + public void setUserDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) { + this.userDetailsContextMapper = userDetailsContextMapper; + } + + /** + * If your users are at a fixed location in the directory (i.e. you can work out the + * DN directly from the username without doing a directory search), you can use this + * attribute to map directly to the DN. It maps directly to the userDnPatterns + * property of AbstractLdapAuthenticator. The value is a specific pattern used to + * build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present + * and will be substituted with the username. + * @param userDnPatterns the LDAP patterns for finding the usernames + */ + public void setUserDnPatterns(String... userDnPatterns) { + this.userDnPatterns = userDnPatterns; + } + + /** + * The LDAP filter used to search for users (optional). For example "(uid={0})". The + * substituted parameter is the user's login name. + * @param userSearchFilter the LDAP filter used to search for users + */ + public void setUserSearchFilter(String userSearchFilter) { + this.userSearchFilter = userSearchFilter; + } + + /** + * Search base for user searches. Defaults to "". Only used with + * {@link #setUserSearchFilter(String)}. + * @param userSearchBase search base for user searches + */ + public void setUserSearchBase(String userSearchBase) { + this.userSearchBase = userSearchBase; + } + + /** + * Returns the configured {@link AuthenticationManager} that can be used to perform + * LDAP authentication. + * @return the configured {@link AuthenticationManager} + */ + public final AuthenticationManager createAuthenticationManager() { + LdapAuthenticationProvider ldapAuthenticationProvider = getProvider(); + return new ProviderManager(ldapAuthenticationProvider); + } + + private LdapAuthenticationProvider getProvider() { + AbstractLdapAuthenticator authenticator = getAuthenticator(); + LdapAuthenticationProvider provider; + if (this.ldapAuthoritiesPopulator != null) { + provider = new LdapAuthenticationProvider(authenticator, this.ldapAuthoritiesPopulator); + } + else { + provider = new LdapAuthenticationProvider(authenticator); + } + if (this.authoritiesMapper != null) { + provider.setAuthoritiesMapper(this.authoritiesMapper); + } + if (this.userDetailsContextMapper != null) { + provider.setUserDetailsContextMapper(this.userDetailsContextMapper); + } + return provider; + } + + private AbstractLdapAuthenticator getAuthenticator() { + AbstractLdapAuthenticator authenticator = createDefaultLdapAuthenticator(); + if (this.userSearchFilter != null) { + authenticator.setUserSearch( + new FilterBasedLdapUserSearch(this.userSearchBase, this.userSearchFilter, this.contextSource)); + } + if (this.userDnPatterns != null && this.userDnPatterns.length > 0) { + authenticator.setUserDnPatterns(this.userDnPatterns); + } + authenticator.afterPropertiesSet(); + return authenticator; + } + + /** + * Allows subclasses to supply the default {@link AbstractLdapAuthenticator}. + * @return the {@link AbstractLdapAuthenticator} that will be configured for LDAP + * authentication + */ + protected abstract T createDefaultLdapAuthenticator(); + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java new file mode 100644 index 00000000000..4a8c2d56d40 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import java.io.IOException; +import java.net.ServerSocket; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.Lifecycle; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.EmbeddedLdapServerContainer; +import org.springframework.security.ldap.server.UnboundIdContainer; +import org.springframework.util.ClassUtils; + +/** + * Creates a {@link DefaultSpringSecurityContextSource} used to perform LDAP + * authentication and starts and in-memory LDAP server. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class EmbeddedLdapServerContextSourceFactoryBean + implements FactoryBean, DisposableBean, ApplicationContextAware { + + private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer"; + + private static final int DEFAULT_PORT = 33389; + + private static final int RANDOM_PORT = 0; + + private Integer port; + + private String ldif = "classpath*:*.ldif"; + + private String root = "dc=springframework,dc=org"; + + private ApplicationContext context; + + private String managerDn; + + private String managerPassword; + + private EmbeddedLdapServerContainer container; + + /** + * Create an EmbeddedLdapServerContextSourceFactoryBean that will use an embedded LDAP + * server to perform LDAP authentication. This requires a dependency on + * `com.unboundid:unboundid-ldapsdk`. + * @return the EmbeddedLdapServerContextSourceFactoryBean + */ + public static EmbeddedLdapServerContextSourceFactoryBean fromEmbeddedLdapServer() { + return new EmbeddedLdapServerContextSourceFactoryBean(); + } + + /** + * Specifies an LDIF to load at startup for an embedded LDAP server. The default is + * "classpath*:*.ldif". + * @param ldif the ldif to load at startup for an embedded LDAP server. + */ + public void setLdif(String ldif) { + this.ldif = ldif; + } + + /** + * The port to connect to LDAP to (the default is 33389 or random available port if + * unavailable). Supplying 0 as the port indicates that a random available port should + * be selected. + * @param port the port to connect to + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Optional root suffix for the embedded LDAP server. Default is + * "dc=springframework,dc=org". + * @param root root suffix for the embedded LDAP server + */ + public void setRoot(String root) { + this.root = root; + } + + /** + * Username (DN) of the "manager" user identity (i.e. "uid=admin,ou=system") which + * will be used to authenticate to an LDAP server. If omitted, anonymous access will + * be used. + * @param managerDn the username (DN) of the "manager" user identity used to + * authenticate to a LDAP server. + */ + public void setManagerDn(String managerDn) { + this.managerDn = managerDn; + } + + /** + * The password for the manager DN. This is required if the + * {@link #setManagerDn(String)} is specified. + * @param managerPassword password for the manager DN + */ + public void setManagerPassword(String managerPassword) { + this.managerPassword = managerPassword; + } + + @Override + public DefaultSpringSecurityContextSource getObject() throws Exception { + if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + this.container = getContainer(); + this.port = this.container.getPort(); + DefaultSpringSecurityContextSource contextSourceFromProviderUrl = new DefaultSpringSecurityContextSource( + "ldap://127.0.0.1:" + this.port + "/" + this.root); + if (this.managerDn != null) { + contextSourceFromProviderUrl.setUserDn(this.managerDn); + if (this.managerPassword == null) { + throw new IllegalStateException("managerPassword is required if managerDn is supplied"); + } + contextSourceFromProviderUrl.setPassword(this.managerPassword); + } + contextSourceFromProviderUrl.afterPropertiesSet(); + return contextSourceFromProviderUrl; + } + + @Override + public Class getObjectType() { + return DefaultSpringSecurityContextSource.class; + } + + @Override + public void destroy() { + if (this.container instanceof Lifecycle) { + ((Lifecycle) this.container).stop(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } + + private EmbeddedLdapServerContainer getContainer() { + if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif); + unboundIdContainer.setApplicationContext(this.context); + unboundIdContainer.setPort(getEmbeddedServerPort()); + unboundIdContainer.afterPropertiesSet(); + return unboundIdContainer; + } + + private int getEmbeddedServerPort() { + if (this.port == null) { + this.port = getDefaultEmbeddedServerPort(); + } + return this.port; + } + + private int getDefaultEmbeddedServerPort() { + try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { + return serverSocket.getLocalPort(); + } + catch (IOException ex) { + return RANDOM_PORT; + } + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java new file mode 100644 index 00000000000..a62fbfab44a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.ldap.authentication.BindAuthenticator; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication using + * bind authentication. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class LdapBindAuthenticationManagerFactory extends AbstractLdapAuthenticationManagerFactory { + + public LdapBindAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) { + super(contextSource); + } + + @Override + protected BindAuthenticator createDefaultLdapAuthenticator() { + return new BindAuthenticator(getContextSource()); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java new file mode 100644 index 00000000000..19c14f998df --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator; +import org.springframework.util.Assert; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication using + * password comparison. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class LdapPasswordComparisonAuthenticationManagerFactory + extends AbstractLdapAuthenticationManagerFactory { + + private PasswordEncoder passwordEncoder; + + private String passwordAttribute; + + public LdapPasswordComparisonAuthenticationManagerFactory(BaseLdapPathContextSource contextSource, + PasswordEncoder passwordEncoder) { + super(contextSource); + setPasswordEncoder(passwordEncoder); + } + + /** + * Specifies the {@link PasswordEncoder} to be used when authenticating with password + * comparison. + * @param passwordEncoder the {@link PasswordEncoder} to use + */ + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder must not be null."); + this.passwordEncoder = passwordEncoder; + } + + /** + * The attribute in the directory which contains the user password. Only used when + * authenticating with password comparison. Defaults to "userPassword". + * @param passwordAttribute the attribute in the directory which contains the user + * password + */ + public void setPasswordAttribute(String passwordAttribute) { + this.passwordAttribute = passwordAttribute; + } + + @Override + protected PasswordComparisonAuthenticator createDefaultLdapAuthenticator() { + PasswordComparisonAuthenticator ldapAuthenticator = new PasswordComparisonAuthenticator(getContextSource()); + if (this.passwordAttribute != null) { + ldapAuthenticator.setPasswordAttributeName(this.passwordAttribute); + } + ldapAuthenticator.setPasswordEncoder(this.passwordEncoder); + return ldapAuthenticator; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java new file mode 100644 index 00000000000..ab55ad0df82 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java @@ -0,0 +1,376 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.saml2; + +import java.io.InputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * @author Marcus da Coregio + * @since 5.7 + */ +public final class RelyingPartyRegistrationsBeanDefinitionParser implements BeanDefinitionParser { + + private static final String ELT_RELYING_PARTY_REGISTRATION = "relying-party-registration"; + + private static final String ELT_SIGNING_CREDENTIAL = "signing-credential"; + + private static final String ELT_DECRYPTION_CREDENTIAL = "decryption-credential"; + + private static final String ELT_ASSERTING_PARTY = "asserting-party"; + + private static final String ELT_VERIFICATION_CREDENTIAL = "verification-credential"; + + private static final String ELT_ENCRYPTION_CREDENTIAL = "encryption-credential"; + + private static final String ATT_REGISTRATION_ID = "registration-id"; + + private static final String ATT_ASSERTING_PARTY_ID = "asserting-party-id"; + + private static final String ATT_ENTITY_ID = "entity-id"; + + private static final String ATT_METADATA_LOCATION = "metadata-location"; + + private static final String ATT_ASSERTION_CONSUMER_SERVICE_LOCATION = "assertion-consumer-service-location"; + + private static final String ATT_ASSERTION_CONSUMER_SERVICE_BINDING = "assertion-consumer-service-binding"; + + private static final String ATT_PRIVATE_KEY_LOCATION = "private-key-location"; + + private static final String ATT_CERTIFICATE_LOCATION = "certificate-location"; + + private static final String ATT_WANT_AUTHN_REQUESTS_SIGNED = "want-authn-requests-signed"; + + private static final String ATT_SINGLE_SIGN_ON_SERVICE_LOCATION = "single-sign-on-service-location"; + + private static final String ATT_SINGLE_SIGN_ON_SERVICE_BINDING = "single-sign-on-service-binding"; + + private static final String ATT_SIGNING_ALGORITHMS = "signing-algorithms"; + + private static final String ATT_SINGLE_LOGOUT_SERVICE_LOCATION = "single-logout-service-location"; + + private static final String ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION = "single-logout-service-response-location"; + + private static final String ATT_SINGLE_LOGOUT_SERVICE_BINDING = "single-logout-service-binding"; + + private static final ResourceLoader resourceLoader = new DefaultResourceLoader(); + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), + parserContext.extractSource(element)); + parserContext.pushContainingComponent(compositeDef); + Map> assertingParties = getAssertingParties(element); + List relyingPartyRegistrations = getRelyingPartyRegistrations(element, + assertingParties, parserContext); + BeanDefinition relyingPartyRegistrationRepositoryBean = BeanDefinitionBuilder + .rootBeanDefinition(InMemoryRelyingPartyRegistrationRepository.class) + .addConstructorArgValue(relyingPartyRegistrations).getBeanDefinition(); + String relyingPartyRegistrationRepositoryId = parserContext.getReaderContext() + .generateBeanName(relyingPartyRegistrationRepositoryBean); + parserContext.registerBeanComponent(new BeanComponentDefinition(relyingPartyRegistrationRepositoryBean, + relyingPartyRegistrationRepositoryId)); + parserContext.popAndRegisterContainingComponent(); + return null; + } + + private static Map> getAssertingParties(Element element) { + List assertingPartyElts = DomUtils.getChildElementsByTagName(element, ELT_ASSERTING_PARTY); + Map> providers = new HashMap<>(); + for (Element assertingPartyElt : assertingPartyElts) { + Map assertingParty = new HashMap<>(); + String assertingPartyId = assertingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID); + String entityId = assertingPartyElt.getAttribute(ATT_ENTITY_ID); + String wantAuthnRequestsSigned = assertingPartyElt.getAttribute(ATT_WANT_AUTHN_REQUESTS_SIGNED); + String singleSignOnServiceLocation = assertingPartyElt.getAttribute(ATT_SINGLE_SIGN_ON_SERVICE_LOCATION); + String singleSignOnServiceBinding = assertingPartyElt.getAttribute(ATT_SINGLE_SIGN_ON_SERVICE_BINDING); + String signingAlgorithms = assertingPartyElt.getAttribute(ATT_SIGNING_ALGORITHMS); + String singleLogoutServiceLocation = assertingPartyElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION); + String singleLogoutServiceResponseLocation = assertingPartyElt + .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION); + String singleLogoutServiceBinding = assertingPartyElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_BINDING); + assertingParty.put(ATT_ASSERTING_PARTY_ID, assertingPartyId); + assertingParty.put(ATT_ENTITY_ID, entityId); + assertingParty.put(ATT_WANT_AUTHN_REQUESTS_SIGNED, wantAuthnRequestsSigned); + assertingParty.put(ATT_SINGLE_SIGN_ON_SERVICE_LOCATION, singleSignOnServiceLocation); + assertingParty.put(ATT_SINGLE_SIGN_ON_SERVICE_BINDING, singleSignOnServiceBinding); + assertingParty.put(ATT_SIGNING_ALGORITHMS, signingAlgorithms); + assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_LOCATION, singleLogoutServiceLocation); + assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION, singleLogoutServiceResponseLocation); + assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_BINDING, singleLogoutServiceBinding); + addVerificationCredentials(assertingPartyElt, assertingParty); + addEncryptionCredentials(assertingPartyElt, assertingParty); + providers.put(assertingPartyId, assertingParty); + } + return providers; + } + + private static void addVerificationCredentials(Map assertingParty, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + List verificationCertificateLocations = (List) assertingParty.get(ELT_VERIFICATION_CREDENTIAL); + List verificationCredentials = new ArrayList<>(); + for (String certificateLocation : verificationCertificateLocations) { + verificationCredentials.add(getSaml2VerificationCredential(certificateLocation)); + } + builder.verificationX509Credentials((credentials) -> credentials.addAll(verificationCredentials)); + } + + private static void addEncryptionCredentials(Map assertingParty, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + List encryptionCertificateLocations = (List) assertingParty.get(ELT_ENCRYPTION_CREDENTIAL); + List encryptionCredentials = new ArrayList<>(); + for (String certificateLocation : encryptionCertificateLocations) { + encryptionCredentials.add(getSaml2EncryptionCredential(certificateLocation)); + } + builder.encryptionX509Credentials((credentials) -> credentials.addAll(encryptionCredentials)); + } + + private static void addVerificationCredentials(Element assertingPartyElt, Map assertingParty) { + List verificationCertificateLocations = new ArrayList<>(); + List verificationCredentialElts = DomUtils.getChildElementsByTagName(assertingPartyElt, + ELT_VERIFICATION_CREDENTIAL); + for (Element verificationCredentialElt : verificationCredentialElts) { + String certificateLocation = verificationCredentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + verificationCertificateLocations.add(certificateLocation); + } + assertingParty.put(ELT_VERIFICATION_CREDENTIAL, verificationCertificateLocations); + } + + private static void addEncryptionCredentials(Element assertingPartyElt, Map assertingParty) { + List encryptionCertificateLocations = new ArrayList<>(); + List encryptionCredentialElts = DomUtils.getChildElementsByTagName(assertingPartyElt, + ELT_VERIFICATION_CREDENTIAL); + for (Element encryptionCredentialElt : encryptionCredentialElts) { + String certificateLocation = encryptionCredentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + encryptionCertificateLocations.add(certificateLocation); + } + assertingParty.put(ELT_ENCRYPTION_CREDENTIAL, encryptionCertificateLocations); + } + + private List getRelyingPartyRegistrations(Element element, + Map> assertingParties, ParserContext parserContext) { + List relyingPartyRegistrationElts = DomUtils.getChildElementsByTagName(element, + ELT_RELYING_PARTY_REGISTRATION); + List relyingPartyRegistrations = new ArrayList<>(); + for (Element relyingPartyRegistrationElt : relyingPartyRegistrationElts) { + RelyingPartyRegistration.Builder builder = getBuilderFromMetadataLocationIfPossible( + relyingPartyRegistrationElt, assertingParties, parserContext); + addSigningCredentials(relyingPartyRegistrationElt, builder); + addDecryptionCredentials(relyingPartyRegistrationElt, builder); + relyingPartyRegistrations.add(builder.build()); + } + return relyingPartyRegistrations; + } + + private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIfPossible( + Element relyingPartyRegistrationElt, Map> assertingParties, + ParserContext parserContext) { + String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID); + String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION); + String singleLogoutServiceLocation = relyingPartyRegistrationElt + .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION); + String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt + .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION); + Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt); + if (StringUtils.hasText(metadataLocation)) { + return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId) + .singleLogoutServiceLocation(singleLogoutServiceLocation) + .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation) + .singleLogoutServiceBinding(singleLogoutServiceBinding); + } + String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID); + String assertionConsumerServiceLocation = relyingPartyRegistrationElt + .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION); + Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding( + relyingPartyRegistrationElt); + return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId) + .assertionConsumerServiceLocation(assertionConsumerServiceLocation) + .assertionConsumerServiceBinding(assertionConsumerServiceBinding) + .singleLogoutServiceLocation(singleLogoutServiceLocation) + .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation) + .singleLogoutServiceBinding(singleLogoutServiceBinding) + .assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties, + builder, parserContext)); + } + + private static void buildAssertingParty(Element relyingPartyElt, Map> assertingParties, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder, ParserContext parserContext) { + String assertingPartyId = relyingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID); + if (!assertingParties.containsKey(assertingPartyId)) { + Object source = parserContext.extractSource(relyingPartyElt); + parserContext.getReaderContext() + .error(String.format("Could not find asserting party with id %s", assertingPartyId), source); + } + Map assertingParty = assertingParties.get(assertingPartyId); + String entityId = getAsString(assertingParty, ATT_ENTITY_ID); + String wantAuthnRequestsSigned = getAsString(assertingParty, ATT_WANT_AUTHN_REQUESTS_SIGNED); + String singleSignOnServiceLocation = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_LOCATION); + String singleSignOnServiceBinding = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_BINDING); + Saml2MessageBinding saml2MessageBinding = StringUtils.hasText(singleSignOnServiceBinding) + ? Saml2MessageBinding.valueOf(singleSignOnServiceBinding) : Saml2MessageBinding.REDIRECT; + String singleLogoutServiceLocation = getAsString(assertingParty, ATT_SINGLE_LOGOUT_SERVICE_LOCATION); + String singleLogoutServiceResponseLocation = getAsString(assertingParty, + ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION); + String singleLogoutServiceBinding = getAsString(assertingParty, ATT_SINGLE_LOGOUT_SERVICE_BINDING); + Saml2MessageBinding saml2LogoutMessageBinding = StringUtils.hasText(singleLogoutServiceBinding) + ? Saml2MessageBinding.valueOf(singleLogoutServiceBinding) : Saml2MessageBinding.REDIRECT; + builder.entityId(entityId).wantAuthnRequestsSigned(Boolean.parseBoolean(wantAuthnRequestsSigned)) + .singleSignOnServiceLocation(singleSignOnServiceLocation) + .singleSignOnServiceBinding(saml2MessageBinding) + .singleLogoutServiceLocation(singleLogoutServiceLocation) + .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation) + .singleLogoutServiceBinding(saml2LogoutMessageBinding); + addSigningAlgorithms(assertingParty, builder); + addVerificationCredentials(assertingParty, builder); + addEncryptionCredentials(assertingParty, builder); + } + + private static void addSigningAlgorithms(Map assertingParty, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + String signingAlgorithmsAttr = getAsString(assertingParty, ATT_SIGNING_ALGORITHMS); + if (StringUtils.hasText(signingAlgorithmsAttr)) { + List signingAlgorithms = Arrays.asList(signingAlgorithmsAttr.split(",")); + builder.signingAlgorithms((s) -> s.addAll(signingAlgorithms)); + } + } + + private static void addSigningCredentials(Element relyingPartyRegistrationElt, + RelyingPartyRegistration.Builder builder) { + List credentialElts = DomUtils.getChildElementsByTagName(relyingPartyRegistrationElt, + ELT_SIGNING_CREDENTIAL); + for (Element credentialElt : credentialElts) { + String privateKeyLocation = credentialElt.getAttribute(ATT_PRIVATE_KEY_LOCATION); + String certificateLocation = credentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + builder.signingX509Credentials( + (c) -> c.add(getSaml2SigningCredential(privateKeyLocation, certificateLocation))); + } + } + + private static void addDecryptionCredentials(Element relyingPartyRegistrationElt, + RelyingPartyRegistration.Builder builder) { + List credentialElts = DomUtils.getChildElementsByTagName(relyingPartyRegistrationElt, + ELT_DECRYPTION_CREDENTIAL); + for (Element credentialElt : credentialElts) { + String privateKeyLocation = credentialElt.getAttribute(ATT_PRIVATE_KEY_LOCATION); + String certificateLocation = credentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + Saml2X509Credential credential = getSaml2DecryptionCredential(privateKeyLocation, certificateLocation); + builder.decryptionX509Credentials((c) -> c.add(credential)); + } + } + + private static String getAsString(Map assertingParty, String key) { + return (String) assertingParty.get(key); + } + + private static Saml2MessageBinding getAssertionConsumerServiceBinding(Element relyingPartyRegistrationElt) { + String assertionConsumerServiceBinding = relyingPartyRegistrationElt + .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_BINDING); + if (StringUtils.hasText(assertionConsumerServiceBinding)) { + return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding); + } + return Saml2MessageBinding.REDIRECT; + } + + private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) { + String singleLogoutServiceBinding = relyingPartyRegistrationElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_BINDING); + if (StringUtils.hasText(singleLogoutServiceBinding)) { + return Saml2MessageBinding.valueOf(singleLogoutServiceBinding); + } + return Saml2MessageBinding.POST; + } + + private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) { + return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.VERIFICATION); + } + + private static Saml2X509Credential getSaml2EncryptionCredential(String certificateLocation) { + return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION); + } + + private static Saml2X509Credential getSaml2SigningCredential(String privateKeyLocation, + String certificateLocation) { + return getSaml2Credential(privateKeyLocation, certificateLocation, + Saml2X509Credential.Saml2X509CredentialType.SIGNING); + } + + private static Saml2X509Credential getSaml2DecryptionCredential(String privateKeyLocation, + String certificateLocation) { + return getSaml2Credential(privateKeyLocation, certificateLocation, + Saml2X509Credential.Saml2X509CredentialType.DECRYPTION); + } + + private static Saml2X509Credential getSaml2Credential(String privateKeyLocation, String certificateLocation, + Saml2X509Credential.Saml2X509CredentialType credentialType) { + RSAPrivateKey privateKey = readPrivateKey(privateKeyLocation); + X509Certificate certificate = readCertificate(certificateLocation); + return new Saml2X509Credential(privateKey, certificate, credentialType); + } + + private static Saml2X509Credential getSaml2Credential(String certificateLocation, + Saml2X509Credential.Saml2X509CredentialType credentialType) { + X509Certificate certificate = readCertificate(certificateLocation); + return new Saml2X509Credential(certificate, credentialType); + } + + private static RSAPrivateKey readPrivateKey(String privateKeyLocation) { + Resource privateKey = resourceLoader.getResource(privateKeyLocation); + try (InputStream inputStream = privateKey.getInputStream()) { + return RsaKeyConverters.pkcs8().convert(inputStream); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + private static X509Certificate readCertificate(String certificateLocation) { + Resource certificate = resourceLoader.getResource(certificateLocation); + try (InputStream inputStream = certificate.getInputStream()) { + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + +} diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 282896dd836..5c0094f079a 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -133,6 +133,7 @@ import org.springframework.security.web.server.authorization.AuthorizationWebFilter; import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; +import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; @@ -148,6 +149,12 @@ import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy; +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy; +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy; import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter; import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter; @@ -1682,6 +1689,18 @@ public AuthorizeExchangeSpec authenticated() { return access(AuthenticatedReactiveAuthorizationManager.authenticated()); } + /** + * Require a specific IP address or range using an IP/Netmask (e.g. + * 192.168.1.0/24). + * @param ipAddress the address or range of addresses from which the request + * must come. + * @return the {@link AuthorizeExchangeSpec} to configure + * @since 5.7 + */ + public AuthorizeExchangeSpec hasIpAddress(String ipAddress) { + return access(IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress)); + } + /** * Allows plugging in a custom authorization strategy * @param manager the authorization manager to use @@ -2367,10 +2386,17 @@ public final class HeaderSpec { private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter(); + private CrossOriginOpenerPolicyServerHttpHeadersWriter crossOriginOpenerPolicy = new CrossOriginOpenerPolicyServerHttpHeadersWriter(); + + private CrossOriginEmbedderPolicyServerHttpHeadersWriter crossOriginEmbedderPolicy = new CrossOriginEmbedderPolicyServerHttpHeadersWriter(); + + private CrossOriginResourcePolicyServerHttpHeadersWriter crossOriginResourcePolicy = new CrossOriginResourcePolicyServerHttpHeadersWriter(); + private HeaderSpec() { this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts, this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy, - this.referrerPolicy)); + this.referrerPolicy, this.crossOriginOpenerPolicy, this.crossOriginEmbedderPolicy, + this.crossOriginResourcePolicy)); } /** @@ -2582,6 +2608,84 @@ public HeaderSpec referrerPolicy(Customizer referrerPolicyCu return this; } + /** + * Configures the + * Cross-Origin-Opener-Policy header. + * @return the {@link CrossOriginOpenerPolicySpec} to configure + * @since 5.7 + * @see CrossOriginOpenerPolicyServerHttpHeadersWriter + */ + public CrossOriginOpenerPolicySpec crossOriginOpenerPolicy() { + return new CrossOriginOpenerPolicySpec(); + } + + /** + * Configures the + * Cross-Origin-Opener-Policy header. + * @return the {@link HeaderSpec} to customize + * @since 5.7 + * @see CrossOriginOpenerPolicyServerHttpHeadersWriter + */ + public HeaderSpec crossOriginOpenerPolicy( + Customizer crossOriginOpenerPolicyCustomizer) { + crossOriginOpenerPolicyCustomizer.customize(new CrossOriginOpenerPolicySpec()); + return this; + } + + /** + * Configures the + * Cross-Origin-Embedder-Policy header. + * @return the {@link CrossOriginEmbedderPolicySpec} to configure + * @since 5.7 + * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter + */ + public CrossOriginEmbedderPolicySpec crossOriginEmbedderPolicy() { + return new CrossOriginEmbedderPolicySpec(); + } + + /** + * Configures the + * Cross-Origin-Embedder-Policy header. + * @return the {@link HeaderSpec} to customize + * @since 5.7 + * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter + */ + public HeaderSpec crossOriginEmbedderPolicy( + Customizer crossOriginEmbedderPolicyCustomizer) { + crossOriginEmbedderPolicyCustomizer.customize(new CrossOriginEmbedderPolicySpec()); + return this; + } + + /** + * Configures the + * Cross-Origin-Resource-Policy header. + * @return the {@link CrossOriginResourcePolicySpec} to configure + * @since 5.7 + * @see CrossOriginResourcePolicyServerHttpHeadersWriter + */ + public CrossOriginResourcePolicySpec crossOriginResourcePolicy() { + return new CrossOriginResourcePolicySpec(); + } + + /** + * Configures the + * Cross-Origin-Resource-Policy header. + * @return the {@link HeaderSpec} to customize + * @since 5.7 + * @see CrossOriginResourcePolicyServerHttpHeadersWriter + */ + public HeaderSpec crossOriginResourcePolicy( + Customizer crossOriginResourcePolicyCustomizer) { + crossOriginResourcePolicyCustomizer.customize(new CrossOriginResourcePolicySpec()); + return this; + } + /** * Configures cache control headers * @@ -2897,6 +3001,99 @@ public HeaderSpec and() { } + /** + * Configures the Cross-Origin-Opener-Policy header + * + * @since 5.7 + */ + public final class CrossOriginOpenerPolicySpec { + + private CrossOriginOpenerPolicySpec() { + } + + /** + * Sets the value to be used in the `Cross-Origin-Opener-Policy` header + * @param openerPolicy a opener policy + * @return the {@link CrossOriginOpenerPolicySpec} to continue configuring + */ + public CrossOriginOpenerPolicySpec policy(CrossOriginOpenerPolicy openerPolicy) { + HeaderSpec.this.crossOriginOpenerPolicy.setPolicy(openerPolicy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + + /** + * Configures the Cross-Origin-Embedder-Policy header + * + * @since 5.7 + */ + public final class CrossOriginEmbedderPolicySpec { + + private CrossOriginEmbedderPolicySpec() { + } + + /** + * Sets the value to be used in the `Cross-Origin-Embedder-Policy` header + * @param embedderPolicy a opener policy + * @return the {@link CrossOriginEmbedderPolicySpec} to continue configuring + */ + public CrossOriginEmbedderPolicySpec policy(CrossOriginEmbedderPolicy embedderPolicy) { + HeaderSpec.this.crossOriginEmbedderPolicy.setPolicy(embedderPolicy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + + /** + * Configures the Cross-Origin-Resource-Policy header + * + * @since 5.7 + */ + public final class CrossOriginResourcePolicySpec { + + private CrossOriginResourcePolicySpec() { + } + + /** + * Sets the value to be used in the `Cross-Origin-Resource-Policy` header + * @param resourcePolicy a opener policy + * @return the {@link CrossOriginResourcePolicySpec} to continue configuring + */ + public CrossOriginResourcePolicySpec policy(CrossOriginResourcePolicy resourcePolicy) { + HeaderSpec.this.crossOriginResourcePolicy.setPolicy(resourcePolicy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + } /** diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java index ba8d3ac4e06..246235a955a 100644 --- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java @@ -19,6 +19,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.w3c.dom.Element; @@ -37,20 +38,34 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; +import org.springframework.security.access.expression.ExpressionUtils; +import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.vote.ConsensusBased; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.Elements; +import org.springframework.security.core.Authentication; import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory; +import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler; import org.springframework.security.messaging.access.expression.MessageExpressionVoter; +import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor; +import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver; import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; +import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher; import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor; import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor; import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -99,6 +114,10 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements private static final String DISABLED_ATTR = "same-origin-disabled"; + private static final String USE_AUTHORIZATION_MANAGER_ATTR = "use-authorization-manager"; + + private static final String AUTHORIZATION_MANAGER_REF_ATTR = "authorization-manager-ref"; + private static final String PATTERN_ATTR = "pattern"; private static final String ACCESS_ATTR = "access"; @@ -114,14 +133,83 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements */ @Override public BeanDefinition parse(Element element, ParserContext parserContext) { + String id = element.getAttribute(ID_ATTR); + String inSecurityInterceptorName = parseAuthorization(element, parserContext); + BeanDefinitionRegistry registry = parserContext.getRegistry(); + if (StringUtils.hasText(id)) { + registry.registerAlias(inSecurityInterceptorName, id); + if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) { + registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class)); + } + } + else { + boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR)); + XmlReaderContext context = parserContext.getReaderContext(); + BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class); + mspp.addConstructorArgValue(inSecurityInterceptorName); + mspp.addConstructorArgValue(sameOriginDisabled); + context.registerWithGeneratedName(mspp.getBeanDefinition()); + } + return null; + } + + private String parseAuthorization(Element element, ParserContext parserContext) { + boolean useAuthorizationManager = Boolean.parseBoolean(element.getAttribute(USE_AUTHORIZATION_MANAGER_ATTR)); + if (useAuthorizationManager) { + return parseAuthorizationManager(element, parserContext); + } + if (StringUtils.hasText(element.getAttribute(AUTHORIZATION_MANAGER_REF_ATTR))) { + return parseAuthorizationManager(element, parserContext); + } + return parseSecurityMetadataSource(element, parserContext); + } + + private String parseAuthorizationManager(Element element, ParserContext parserContext) { + XmlReaderContext context = parserContext.getReaderContext(); + String mdsId = createAuthorizationManager(element, parserContext); + BeanDefinitionBuilder inboundChannelSecurityInterceptor = BeanDefinitionBuilder + .rootBeanDefinition(AuthorizationChannelInterceptor.class); + inboundChannelSecurityInterceptor.addConstructorArgReference(mdsId); + return context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition()); + } + + private String createAuthorizationManager(Element element, ParserContext parserContext) { + XmlReaderContext context = parserContext.getReaderContext(); + String authorizationManagerRef = element.getAttribute(AUTHORIZATION_MANAGER_REF_ATTR); + if (StringUtils.hasText(authorizationManagerRef)) { + return authorizationManagerRef; + } + Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER); + String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null; + ManagedMap matcherToExpression = new ManagedMap<>(); + List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE); + for (Element interceptMessage : interceptMessages) { + String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR); + String accessExpression = interceptMessage.getAttribute(ACCESS_ATTR); + String messageType = interceptMessage.getAttribute(TYPE_ATTR); + BeanDefinition matcher = createMatcher(matcherPattern, messageType, parserContext, interceptMessage); + BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder + .rootBeanDefinition(ExpressionBasedAuthorizationManager.class); + if (StringUtils.hasText(expressionHandlerRef)) { + authorizationManager.addConstructorArgReference(expressionHandlerRef); + } + authorizationManager.addConstructorArgValue(accessExpression); + matcherToExpression.put(matcher, authorizationManager.getBeanDefinition()); + } + BeanDefinitionBuilder mds = BeanDefinitionBuilder + .rootBeanDefinition(MessageMatcherDelegatingAuthorizationManagerFactory.class); + mds.setFactoryMethod("createMessageMatcherDelegatingAuthorizationManager"); + mds.addConstructorArgValue(matcherToExpression); + return context.registerWithGeneratedName(mds.getBeanDefinition()); + } + + private String parseSecurityMetadataSource(Element element, ParserContext parserContext) { BeanDefinitionRegistry registry = parserContext.getRegistry(); XmlReaderContext context = parserContext.getReaderContext(); ManagedMap matcherToExpression = new ManagedMap<>(); - String id = element.getAttribute(ID_ATTR); Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER); String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null; boolean expressionHandlerDefined = StringUtils.hasText(expressionHandlerRef); - boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR)); List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE); for (Element interceptMessage : interceptMessages) { String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR); @@ -151,21 +239,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { .rootBeanDefinition(ChannelSecurityInterceptor.class); inboundChannelSecurityInterceptor.addConstructorArgValue(registry.getBeanDefinition(mdsId)); inboundChannelSecurityInterceptor.addPropertyValue("accessDecisionManager", adm.getBeanDefinition()); - String inSecurityInterceptorName = context - .registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition()); - if (StringUtils.hasText(id)) { - registry.registerAlias(inSecurityInterceptorName, id); - if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) { - registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class)); - } - } - else { - BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class); - mspp.addConstructorArgValue(inSecurityInterceptorName); - mspp.addConstructorArgValue(sameOriginDisabled); - context.registerWithGeneratedName(mspp.getBeanDefinition()); - } - return null; + return context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition()); } private BeanDefinition createMatcher(String matcherPattern, String messageType, ParserContext parserContext, @@ -341,4 +415,48 @@ void setPathMatcher(PathMatcher pathMatcher) { } + private static final class ExpressionBasedAuthorizationManager + implements AuthorizationManager> { + + private final SecurityExpressionHandler> expressionHandler; + + private final Expression expression; + + private ExpressionBasedAuthorizationManager(String expression) { + this(new MessageAuthorizationContextSecurityExpressionHandler(), expression); + } + + private ExpressionBasedAuthorizationManager( + SecurityExpressionHandler> expressionHandler, String expression) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + Assert.notNull(expression, "expression cannot be null"); + this.expressionHandler = expressionHandler; + this.expression = this.expressionHandler.getExpressionParser().parseExpression(expression); + } + + @Override + public AuthorizationDecision check(Supplier authentication, + MessageAuthorizationContext object) { + EvaluationContext context = this.expressionHandler.createEvaluationContext(authentication, object); + boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, context); + return new AuthorizationDecision(granted); + } + + } + + private static class MessageMatcherDelegatingAuthorizationManagerFactory { + + private static AuthorizationManager> createMessageMatcherDelegatingAuthorizationManager( + Map, AuthorizationManager>> beans) { + MessageMatcherDelegatingAuthorizationManager.Builder builder = MessageMatcherDelegatingAuthorizationManager + .builder(); + for (Map.Entry, AuthorizationManager>> entry : beans + .entrySet()) { + builder.matchers(entry.getKey()).access(entry.getValue()); + } + return builder.anyMessage().permitAll().build(); + } + + } + } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt index 8df31aaca51..bfd029ec986 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt @@ -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. @@ -22,6 +22,7 @@ import org.springframework.security.authorization.AuthorizationDecision import org.springframework.security.authorization.ReactiveAuthorizationManager import org.springframework.security.core.Authentication import org.springframework.security.web.server.authorization.AuthorizationContext +import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers import org.springframework.security.web.util.matcher.RequestMatcher @@ -108,6 +109,13 @@ class AuthorizeExchangeDsl { fun hasAnyAuthority(vararg authorities: String): ReactiveAuthorizationManager = AuthorityReactiveAuthorizationManager.hasAnyAuthority(*authorities) + /** + * Require a specific IP or range of IP addresses. + * @since 5.7 + */ + fun hasIpAddress(ipAddress: String): ReactiveAuthorizationManager = + IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress) + /** * Require an authenticated user. */ diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt new file mode 100644 index 00000000000..cf5ae7ec9d9 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Embedder-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerCrossOriginEmbedderPolicyDsl { + + var policy: CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit { + return { crossOriginEmbedderPolicy -> + policy?.also { + crossOriginEmbedderPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt new file mode 100644 index 00000000000..70d6576c837 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Opener-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerCrossOriginOpenerPolicyDsl { + + var policy: CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit { + return { crossOriginOpenerPolicy -> + policy?.also { + crossOriginOpenerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt new file mode 100644 index 00000000000..580ee355ee7 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Resource-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerCrossOriginResourcePolicyDsl { + + var policy: CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit { + return { crossOriginResourcePolicy -> + policy?.also { + crossOriginResourcePolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt index f38b1527218..37bd1f177a9 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt @@ -16,7 +16,12 @@ package org.springframework.security.config.web.server -import org.springframework.security.web.server.header.* +import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter /** * A Kotlin DSL to configure [ServerHttpSecurity] headers using idiomatic Kotlin code. @@ -35,6 +40,9 @@ class ServerHeadersDsl { private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null private var featurePolicyDirectives: String? = null private var permissionsPolicy: ((ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit)? = null + private var crossOriginOpenerPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit)? = null + private var crossOriginEmbedderPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit)? = null + private var crossOriginResourcePolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit)? = null private var disabled = false @@ -157,6 +165,39 @@ class ServerHeadersDsl { this.permissionsPolicy = ServerPermissionsPolicyDsl().apply(permissionsPolicyConfig).get() } + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + * + * @since 5.7 + * @param crossOriginOpenerPolicyConfig the customization to apply to the header + */ + fun crossOriginOpenerPolicy(crossOriginOpenerPolicyConfig: ServerCrossOriginOpenerPolicyDsl.() -> Unit) { + this.crossOriginOpenerPolicy = ServerCrossOriginOpenerPolicyDsl().apply(crossOriginOpenerPolicyConfig).get() + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + * + * @since 5.7 + * @param crossOriginEmbedderPolicyConfig the customization to apply to the header + */ + fun crossOriginEmbedderPolicy(crossOriginEmbedderPolicyConfig: ServerCrossOriginEmbedderPolicyDsl.() -> Unit) { + this.crossOriginEmbedderPolicy = ServerCrossOriginEmbedderPolicyDsl().apply(crossOriginEmbedderPolicyConfig).get() + } + + /** + * Allows configuration for + * Cross-Origin-Resource-Policy header. + * + * @since 5.7 + * @param crossOriginResourcePolicyConfig the customization to apply to the header + */ + fun crossOriginResourcePolicy(crossOriginResourcePolicyConfig: ServerCrossOriginResourcePolicyDsl.() -> Unit) { + this.crossOriginResourcePolicy = ServerCrossOriginResourcePolicyDsl().apply(crossOriginResourcePolicyConfig).get() + } + /** * Disables HTTP response headers. */ @@ -194,6 +235,15 @@ class ServerHeadersDsl { referrerPolicy?.also { headers.referrerPolicy(referrerPolicy) } + crossOriginOpenerPolicy?.also { + headers.crossOriginOpenerPolicy(crossOriginOpenerPolicy) + } + crossOriginEmbedderPolicy?.also { + headers.crossOriginEmbedderPolicy(crossOriginEmbedderPolicy) + } + crossOriginResourcePolicy?.also { + headers.crossOriginResourcePolicy(crossOriginResourcePolicy) + } if (disabled) { headers.disable() } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt index 9e35287b5f5..3fd1f4ea283 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt @@ -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. @@ -17,6 +17,8 @@ package org.springframework.security.config.web.servlet import org.springframework.http.HttpMethod +import org.springframework.security.authorization.AuthorizationManager +import org.springframework.security.web.access.intercept.RequestAuthorizationContext import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher @@ -36,14 +38,25 @@ abstract class AbstractRequestMatcherDsl { protected data class MatcherAuthorizationRule(val matcher: RequestMatcher, override val rule: String) : AuthorizationRule(rule) + protected data class MatcherAuthorizationManagerRule(val matcher: RequestMatcher, + override val rule: AuthorizationManager) : AuthorizationManagerRule(rule) + protected data class PatternAuthorizationRule(val pattern: String, val patternType: PatternType, val servletPath: String? = null, val httpMethod: HttpMethod? = null, override val rule: String) : AuthorizationRule(rule) + protected data class PatternAuthorizationManagerRule(val pattern: String, + val patternType: PatternType, + val servletPath: String? = null, + val httpMethod: HttpMethod? = null, + override val rule: AuthorizationManager) : AuthorizationManagerRule(rule) + protected abstract class AuthorizationRule(open val rule: String) + protected abstract class AuthorizationManagerRule(open val rule: AuthorizationManager) + protected enum class PatternType { ANT, MVC } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt new file mode 100644 index 00000000000..f0ac9f4971e --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDsl.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet + +import org.springframework.http.HttpMethod +import org.springframework.security.authorization.AuthenticatedAuthorizationManager +import org.springframework.security.authorization.AuthorityAuthorizationManager +import org.springframework.security.authorization.AuthorizationDecision +import org.springframework.security.authorization.AuthorizationManager +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer +import org.springframework.security.core.Authentication +import org.springframework.security.web.access.intercept.AuthorizationFilter +import org.springframework.security.web.access.intercept.RequestAuthorizationContext +import org.springframework.security.web.util.matcher.AnyRequestMatcher +import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.util.ClassUtils +import java.util.function.Supplier + +/** + * A Kotlin DSL to configure [HttpSecurity] request authorization using idiomatic Kotlin code. + * + * @author Yuriy Savchenko + * @since 5.7 + * @property shouldFilterAllDispatcherTypes whether the [AuthorizationFilter] should filter all dispatcher types + */ +class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { + var shouldFilterAllDispatcherTypes: Boolean? = null + + private val authorizationRules = mutableListOf() + + private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" + private val MVC_PRESENT = ClassUtils.isPresent( + HANDLER_MAPPING_INTROSPECTOR, + AuthorizeHttpRequestsDsl::class.java.classLoader) + private val PATTERN_TYPE = if (MVC_PRESENT) PatternType.MVC else PatternType.ANT + + /** + * Adds a request authorization rule. + * + * @param matches the [RequestMatcher] to match incoming requests against + * @param access the [AuthorizationManager] to secure the matching request + * (i.e. created via hasAuthority("ROLE_USER")) + */ + fun authorize(matches: RequestMatcher = AnyRequestMatcher.INSTANCE, + access: AuthorizationManager) { + authorizationRules.add(MatcherAuthorizationManagerRule(matches, access)) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param pattern the pattern to match incoming requests against. + * @param access the [AuthorizationManager] to secure the matching request + * (i.e. created via hasAuthority("ROLE_USER")) + */ + fun authorize(pattern: String, + access: AuthorizationManager) { + authorizationRules.add( + PatternAuthorizationManagerRule( + pattern = pattern, + patternType = PATTERN_TYPE, + rule = access + ) + ) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param method the HTTP method to match the income requests against. + * @param pattern the pattern to match incoming requests against. + * @param access the [AuthorizationManager] to secure the matching request + * (i.e. created via hasAuthority("ROLE_USER")) + */ + fun authorize(method: HttpMethod, + pattern: String, + access: AuthorizationManager) { + authorizationRules.add( + PatternAuthorizationManagerRule( + pattern = pattern, + patternType = PATTERN_TYPE, + httpMethod = method, + rule = access + ) + ) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param pattern the pattern to match incoming requests against. + * @param servletPath the servlet path to match incoming requests against. This + * only applies when using an MVC pattern matcher. + * @param access the [AuthorizationManager] to secure the matching request + * (i.e. created via hasAuthority("ROLE_USER")) + */ + fun authorize(pattern: String, + servletPath: String, + access: AuthorizationManager) { + authorizationRules.add( + PatternAuthorizationManagerRule( + pattern = pattern, + patternType = PATTERN_TYPE, + servletPath = servletPath, + rule = access + ) + ) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param method the HTTP method to match the income requests against. + * @param pattern the pattern to match incoming requests against. + * @param servletPath the servlet path to match incoming requests against. This + * only applies when using an MVC pattern matcher. + * @param access the [AuthorizationManager] to secure the matching request + * (i.e. created via hasAuthority("ROLE_USER")) + */ + fun authorize(method: HttpMethod, + pattern: String, + servletPath: String, + access: AuthorizationManager) { + authorizationRules.add( + PatternAuthorizationManagerRule( + pattern = pattern, + patternType = PATTERN_TYPE, + servletPath = servletPath, + httpMethod = method, + rule = access + ) + ) + } + + /** + * Specify that URLs require a particular authority. + * + * @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, etc). + * @return the [AuthorizationManager] with the provided authority + */ + fun hasAuthority(authority: String): AuthorizationManager { + return AuthorityAuthorizationManager.hasAuthority(authority) + } + + /** + * Specify that URLs require any of the provided authorities. + * + * @param authorities the authorities to require (i.e. ROLE_USER, ROLE_ADMIN, etc). + * @return the [AuthorizationManager] with the provided authorities + */ + fun hasAnyAuthority(vararg authorities: String): AuthorizationManager { + return AuthorityAuthorizationManager.hasAnyAuthority(*authorities) + } + + /** + * Specify that URLs require a particular role. + * + * @param role the role to require (i.e. USER, ADMIN, etc). + * @return the [AuthorizationManager] with the provided role + */ + fun hasRole(role: String): AuthorizationManager { + return AuthorityAuthorizationManager.hasRole(role) + } + + /** + * Specify that URLs require any of the provided roles. + * + * @param roles the roles to require (i.e. USER, ADMIN, etc). + * @return the [AuthorizationManager] with the provided roles + */ + fun hasAnyRole(vararg roles: String): AuthorizationManager { + return AuthorityAuthorizationManager.hasAnyRole(*roles) + } + + /** + * Specify that URLs are allowed by anyone. + */ + val permitAll: AuthorizationManager = + AuthorizationManager { _: Supplier, _: RequestAuthorizationContext -> AuthorizationDecision(true) } + + /** + * Specify that URLs are not allowed by anyone. + */ + val denyAll: AuthorizationManager = + AuthorizationManager { _: Supplier, _: RequestAuthorizationContext -> AuthorizationDecision(false) } + + /** + * Specify that URLs are allowed by any authenticated user. + */ + val authenticated: AuthorizationManager = + AuthenticatedAuthorizationManager.authenticated() + + internal fun get(): (AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry) -> Unit { + return { requests -> + authorizationRules.forEach { rule -> + when (rule) { + is MatcherAuthorizationManagerRule -> requests.requestMatchers(rule.matcher).access(rule.rule) + is PatternAuthorizationManagerRule -> { + when (rule.patternType) { + PatternType.ANT -> requests.antMatchers(rule.httpMethod, rule.pattern).access(rule.rule) + PatternType.MVC -> requests.mvcMatchers(rule.httpMethod, rule.pattern) + .apply { if (rule.servletPath != null) servletPath(rule.servletPath) } + .access(rule.rule) + } + } + } + } + shouldFilterAllDispatcherTypes?.also { shouldFilter -> + requests.shouldFilterAllDispatcherTypes(shouldFilter) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt index 663287a1184..adb8fef30cc 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt @@ -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. @@ -54,7 +54,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { * Adds a request authorization rule for an endpoint matching the provided * pattern. * If Spring MVC is on the classpath, it will use an MVC matcher. - * If Spring MVC is not an the classpath, it will use an ant matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. * The MVC will use the same rules that Spring MVC uses for matching. * For example, often times a mapping of the path "/path" will match on * "/path", "/path/", "/path.html", etc. @@ -75,7 +75,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { * Adds a request authorization rule for an endpoint matching the provided * pattern. * If Spring MVC is on the classpath, it will use an MVC matcher. - * If Spring MVC is not an the classpath, it will use an ant matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. * The MVC will use the same rules that Spring MVC uses for matching. * For example, often times a mapping of the path "/path" will match on * "/path", "/path/", "/path.html", etc. @@ -98,7 +98,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { * Adds a request authorization rule for an endpoint matching the provided * pattern. * If Spring MVC is on the classpath, it will use an MVC matcher. - * If Spring MVC is not an the classpath, it will use an ant matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. * The MVC will use the same rules that Spring MVC uses for matching. * For example, often times a mapping of the path "/path" will match on * "/path", "/path/", "/path.html", etc. @@ -122,7 +122,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { * Adds a request authorization rule for an endpoint matching the provided * pattern. * If Spring MVC is on the classpath, it will use an MVC matcher. - * If Spring MVC is not an the classpath, it will use an ant matcher. + * If Spring MVC is not on the classpath, it will use an ant matcher. * The MVC will use the same rules that Spring MVC uses for matching. * For example, often times a mapping of the path "/path" will match on * "/path", "/path/", "/path.html", etc. @@ -154,7 +154,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { fun hasAuthority(authority: String) = "hasAuthority('$authority')" /** - * Specify that URLs requires any of a number authorities. + * Specify that URLs require any number of authorities. * * @param authorities the authorities to require (i.e. ROLE_USER, ROLE_ADMIN, etc). * @return the SpEL expression "hasAnyAuthority" with the given authorities as a @@ -175,7 +175,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { fun hasRole(role: String) = "hasRole('$role')" /** - * Specify that URLs requires any of a number roles. + * Specify that URLs require any number of roles. * * @param roles the roles to require (i.e. USER, ADMIN, etc). * @return the SpEL expression "hasAnyRole" with the given roles as a diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt index 3079dd11ff3..d7dd4639294 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt @@ -42,6 +42,9 @@ class HeadersDsl { private var referrerPolicy: ((HeadersConfigurer.ReferrerPolicyConfig) -> Unit)? = null private var featurePolicyDirectives: String? = null private var permissionsPolicy: ((HeadersConfigurer.PermissionsPolicyConfig) -> Unit)? = null + private var crossOriginOpenerPolicy: ((HeadersConfigurer.CrossOriginOpenerPolicyConfig) -> Unit)? = null + private var crossOriginEmbedderPolicy: ((HeadersConfigurer.CrossOriginEmbedderPolicyConfig) -> Unit)? = null + private var crossOriginResourcePolicy: ((HeadersConfigurer.CrossOriginResourcePolicyConfig) -> Unit)? = null private var disabled = false private var headerWriters = mutableListOf() @@ -181,6 +184,54 @@ class HeadersDsl { this.permissionsPolicy = PermissionsPolicyDsl().apply(permissionsPolicyConfig).get() } + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + * + *

      + * Calling this method automatically enables (includes) the Cross-Origin-Opener-Policy + * header in the response using the supplied policy. + *

      + * + * @since 5.7 + * @param crossOriginOpenerPolicyConfig the customization to apply to the header + */ + fun crossOriginOpenerPolicy(crossOriginOpenerPolicyConfig: CrossOriginOpenerPolicyDsl.() -> Unit) { + this.crossOriginOpenerPolicy = CrossOriginOpenerPolicyDsl().apply(crossOriginOpenerPolicyConfig).get() + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + * + *

      + * Calling this method automatically enables (includes) the Cross-Origin-Embedder-Policy + * header in the response using the supplied policy. + *

      + * + * @since 5.7 + * @param crossOriginEmbedderPolicyConfig the customization to apply to the header + */ + fun crossOriginEmbedderPolicy(crossOriginEmbedderPolicyConfig: CrossOriginEmbedderPolicyDsl.() -> Unit) { + this.crossOriginEmbedderPolicy = CrossOriginEmbedderPolicyDsl().apply(crossOriginEmbedderPolicyConfig).get() + } + + /** + * Configures the + * Cross-Origin-Resource-Policy header. + * + *

      + * Calling this method automatically enables (includes) the Cross-Origin-Resource-Policy + * header in the response using the supplied policy. + *

      + * + * @since 5.7 + * @param crossOriginResourcePolicyConfig the customization to apply to the header + */ + fun crossOriginResourcePolicy(crossOriginResourcePolicyConfig: CrossOriginResourcePolicyDsl.() -> Unit) { + this.crossOriginResourcePolicy = CrossOriginResourcePolicyDsl().apply(crossOriginResourcePolicyConfig).get() + } + /** * Adds a [HeaderWriter] instance. * @@ -238,6 +289,15 @@ class HeadersDsl { permissionsPolicy?.also { headers.permissionsPolicy(permissionsPolicy) } + crossOriginOpenerPolicy?.also { + headers.crossOriginOpenerPolicy(crossOriginOpenerPolicy) + } + crossOriginEmbedderPolicy?.also { + headers.crossOriginEmbedderPolicy(crossOriginEmbedderPolicy) + } + crossOriginResourcePolicy?.also { + headers.crossOriginResourcePolicy(crossOriginResourcePolicy) + } headerWriters.forEach { headerWriter -> headers.addHeaderWriter(headerWriter) } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt index d7a17f72af6..64820cc51c6 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -38,8 +38,8 @@ import javax.servlet.http.HttpServletRequest * override fun configure(http: HttpSecurity) { * http { * authorizeRequests { - * request("/public", permitAll) - * request(anyRequest, authenticated) + * authorize("/public", permitAll) + * authorize(anyRequest, authenticated) * } * formLogin { * loginPage = "/log-in" @@ -50,6 +50,7 @@ import javax.servlet.http.HttpServletRequest * ``` * * @author Eleftheria Stein + * @author Norbert Nowak * @since 5.3 * @param httpConfiguration the configurations to apply to [HttpSecurity] */ @@ -101,7 +102,10 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu fun securityMatcher(vararg pattern: String) { val mvcPresent = ClassUtils.isPresent( HANDLER_MAPPING_INTROSPECTOR, - AuthorizeRequestsDsl::class.java.classLoader) + AuthorizeRequestsDsl::class.java.classLoader) || + ClassUtils.isPresent( + HANDLER_MAPPING_INTROSPECTOR, + AuthorizeHttpRequestsDsl::class.java.classLoader) this.http.requestMatchers { if (mvcPresent) { it.mvcMatchers(*pattern) @@ -181,8 +185,8 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * override fun configure(http: HttpSecurity) { * http { * authorizeRequests { - * request("/public", permitAll) - * request(anyRequest, authenticated) + * authorize("/public", permitAll) + * authorize(anyRequest, authenticated) * } * } * } @@ -198,6 +202,38 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.authorizeRequests(authorizeRequestsCustomizer) } + /** + * Allows restricting access based upon the [HttpServletRequest] + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig { + * + * @Bean + * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + * http { + * authorizeHttpRequests { + * authorize("/public", permitAll) + * authorize(anyRequest, authenticated) + * } + * } + * return http.build() + * } + * } + * ``` + * + * @param authorizeHttpRequestsConfiguration custom configuration that specifies + * access for requests + * @see [AuthorizeHttpRequestsDsl] + * @since 5.7 + */ + fun authorizeHttpRequests(authorizeHttpRequestsConfiguration: AuthorizeHttpRequestsDsl.() -> Unit) { + val authorizeHttpRequestsCustomizer = AuthorizeHttpRequestsDsl().apply(authorizeHttpRequestsConfiguration).get() + this.http.authorizeHttpRequests(authorizeHttpRequestsCustomizer) + } + /** * Enables HTTP basic authentication. * @@ -870,4 +906,32 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu init() authenticationManager?.also { this.http.authenticationManager(authenticationManager) } } + + /** + * Enables security context configuration. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * securityContext { + * securityContextRepository = SECURITY_CONTEXT_REPOSITORY + * } + * } + * } + * } + * ``` + * @author Norbert Nowak + * @since 5.7 + * @param securityContextConfiguration configuration to be applied to Security Context + * @see [SecurityContextDsl] + */ + fun securityContext(securityContextConfiguration: SecurityContextDsl.() -> Unit) { + val securityContextCustomizer = SecurityContextDsl().apply(securityContextConfiguration).get() + this.http.securityContext(securityContextCustomizer) + } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/SecurityContextDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/SecurityContextDsl.kt new file mode 100644 index 00000000000..0236a7b96ed --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/SecurityContextDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.web.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer +import org.springframework.security.web.context.SecurityContextRepository + + +/** + * A Kotlin DSL to configure [HttpSecurity] security context using idiomatic Kotlin code. + * + * @property securityContextRepository the [SecurityContextRepository] used for persisting [org.springframework.security.core.context.SecurityContext] between requests + * @author Norbert Nowak + * @since 5.7 + */ +@SecurityMarker +class SecurityContextDsl { + + var securityContextRepository: SecurityContextRepository? = null + var requireExplicitSave: Boolean? = null + + internal fun get(): (SecurityContextConfigurer) -> Unit { + return { securityContext -> + securityContextRepository?.also { securityContext.securityContextRepository(it) } + requireExplicitSave?.also { securityContext.requireExplicitSave(it) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt new file mode 100644 index 00000000000..facd6e6d028 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Embedder-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class CrossOriginEmbedderPolicyDsl { + + var policy: CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy? = null + + internal fun get(): (HeadersConfigurer.CrossOriginEmbedderPolicyConfig) -> Unit { + return { crossOriginEmbedderPolicy -> + policy?.also { + crossOriginEmbedderPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt new file mode 100644 index 00000000000..ea6c19da50c --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Opener-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class CrossOriginOpenerPolicyDsl { + + var policy: CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy? = null + + internal fun get(): (HeadersConfigurer.CrossOriginOpenerPolicyConfig) -> Unit { + return { crossOriginOpenerPolicy -> + policy?.also { + crossOriginOpenerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt new file mode 100644 index 00000000000..fd582582056 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Resource-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class CrossOriginResourcePolicyDsl { + + var policy: CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy? = null + + internal fun get(): (HeadersConfigurer.CrossOriginResourcePolicyConfig) -> Unit { + return { crossOriginResourcePolicy -> + policy?.also { + crossOriginResourcePolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index a67252965d7..d92e3385b81 100644 --- a/config/src/main/resources/META-INF/spring.schemas +++ b/config/src/main/resources/META-INF/spring.schemas @@ -1,4 +1,6 @@ -http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.6.xsd +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.8.xsd +http\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd +http\://www.springframework.org/schema/security/spring-security-5.7.xsd=org/springframework/security/config/spring-security-5.7.xsd http\://www.springframework.org/schema/security/spring-security-5.6.xsd=org/springframework/security/config/spring-security-5.6.xsd http\://www.springframework.org/schema/security/spring-security-5.5.xsd=org/springframework/security/config/spring-security-5.5.xsd http\://www.springframework.org/schema/security/spring-security-5.4.xsd=org/springframework/security/config/spring-security-5.4.xsd @@ -17,7 +19,9 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd -https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.6.xsd +https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.8.xsd +https\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd +https\://www.springframework.org/schema/security/spring-security-5.7.xsd=org/springframework/security/config/spring-security-5.7.xsd https\://www.springframework.org/schema/security/spring-security-5.6.xsd=org/springframework/security/config/spring-security-5.6.xsd https\://www.springframework.org/schema/security/spring-security-5.5.xsd=org/springframework/security/config/spring-security-5.5.xsd https\://www.springframework.org/schema/security/spring-security-5.4.xsd=org/springframework/security/config/spring-security-5.4.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc new file mode 100644 index 00000000000..0a79d344e8a --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc @@ -0,0 +1,1321 @@ +namespace a = "https://relaxng.org/ns/compatibility/annotations/1.0" +datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" + +default namespace = "http://www.springframework.org/schema/security" + +start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider + +hash = + ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + attribute hash {"bcrypt"} +base64 = + ## Whether a string should be base64 encoded + attribute base64 {xsd:boolean} +request-matcher = + ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. + attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} +port = + ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. + attribute port { xsd:nonNegativeInteger } +url = + ## Specifies a URL. + attribute url { xsd:token } +id = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute id {xsd:token} +name = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute name {xsd:token} +ref = + ## Defines a reference to a Spring bean Id. + attribute ref {xsd:token} + +cache-ref = + ## Defines a reference to a cache for use with a UserDetailsService. + attribute cache-ref {xsd:token} + +user-service-ref = + ## A reference to a user-service (or UserDetailsService bean) Id + attribute user-service-ref {xsd:token} + +authentication-manager-ref = + ## A reference to an AuthenticationManager bean + attribute authentication-manager-ref {xsd:token} + +data-source-ref = + ## A reference to a DataSource bean + attribute data-source-ref {xsd:token} + + + +debug = + ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. + element debug {empty} + +password-encoder = + ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. + element password-encoder {password-encoder.attlist} +password-encoder.attlist &= + ref | (hash) + +role-prefix = + ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. + attribute role-prefix {xsd:token} + +use-expressions = + ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. + attribute use-expressions {xsd:boolean} + +ldap-server = + ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. + element ldap-server {ldap-server.attlist} +ldap-server.attlist &= id? +ldap-server.attlist &= (url | port)? +ldap-server.attlist &= + ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. + attribute manager-dn {xsd:string}? +ldap-server.attlist &= + ## The password for the manager DN. This is required if the manager-dn is specified. + attribute manager-password {xsd:string}? +ldap-server.attlist &= + ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff + attribute ldif { xsd:string }? +ldap-server.attlist &= + ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + attribute root { xsd:string }? +ldap-server.attlist &= + ## Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and 'unboundid'. By default, it will depends if the library is available in the classpath. + attribute mode { "apacheds" | "unboundid" }? + +ldap-server-ref-attribute = + ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + attribute server-ref {xsd:token} + + +group-search-filter-attribute = + ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. + attribute group-search-filter {xsd:token} +group-search-base-attribute = + ## Search base for group membership searches. Defaults to "" (searching from the root). + attribute group-search-base {xsd:token} +user-search-filter-attribute = + ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. + attribute user-search-filter {xsd:token} +user-search-base-attribute = + ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + attribute user-search-base {xsd:token} +group-role-attribute-attribute = + ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". + attribute group-role-attribute {xsd:token} +user-details-class-attribute = + ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + attribute user-details-class {"person" | "inetOrgPerson"} +user-context-mapper-attribute = + ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + attribute user-context-mapper-ref {xsd:token} + + +ldap-user-service = + ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + element ldap-user-service {ldap-us.attlist} +ldap-us.attlist &= id? +ldap-us.attlist &= + ldap-server-ref-attribute? +ldap-us.attlist &= + user-search-filter-attribute? +ldap-us.attlist &= + user-search-base-attribute? +ldap-us.attlist &= + group-search-filter-attribute? +ldap-us.attlist &= + group-search-base-attribute? +ldap-us.attlist &= + group-role-attribute-attribute? +ldap-us.attlist &= + cache-ref? +ldap-us.attlist &= + role-prefix? +ldap-us.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +ldap-authentication-provider = + ## Sets up an ldap authentication provider + element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} +ldap-ap.attlist &= + ldap-server-ref-attribute? +ldap-ap.attlist &= + user-search-base-attribute? +ldap-ap.attlist &= + user-search-filter-attribute? +ldap-ap.attlist &= + group-search-base-attribute? +ldap-ap.attlist &= + group-search-filter-attribute? +ldap-ap.attlist &= + group-role-attribute-attribute? +ldap-ap.attlist &= + ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. + attribute user-dn-pattern {xsd:token}? +ldap-ap.attlist &= + role-prefix? +ldap-ap.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +password-compare-element = + ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user + element password-compare {password-compare.attlist, password-encoder?} + +password-compare.attlist &= + ## The attribute in the directory which contains the user password. Defaults to "userPassword". + attribute password-attribute {xsd:token}? +password-compare.attlist &= + hash? + +intercept-methods = + ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + element intercept-methods {intercept-methods.attlist, protect+} +intercept-methods.attlist &= + ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + attribute access-decision-manager-ref {xsd:token}? + + +protect = + ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + element protect {protect.attlist, empty} +protect.attlist &= + ## A method name + attribute method {xsd:token} +protect.attlist &= + ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + attribute access {xsd:token} + +method-security-metadata-source = + ## Creates a MethodSecurityMetadataSource instance + element method-security-metadata-source {msmds.attlist, protect+} +msmds.attlist &= id? + +msmds.attlist &= use-expressions? + +method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + element method-security {method-security.attlist, expression-handler?} +method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". + attribute pre-post-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false". + attribute secured-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false". + attribute jsr250-enabled {xsd:boolean}? +method-security.attlist &= + ## If true, class-based proxying will be used instead of interface-based proxying. + attribute proxy-target-class {xsd:boolean}? + +global-method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. + element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". + attribute pre-post-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". + attribute secured-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". + attribute jsr250-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Optional AccessDecisionManager bean ID to override the default used for method security. + attribute access-decision-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor + attribute run-as-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Allows the advice "order" to be set for the method security interceptor. + attribute order {xsd:token}? +global-method-security.attlist &= + ## If true, class based proxying will be used instead of interface based proxying. + attribute proxy-target-class {xsd:boolean}? +global-method-security.attlist &= + ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. + attribute mode {"aspectj"}? +global-method-security.attlist &= + ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). + attribute metadata-source-ref {xsd:token}? +global-method-security.attlist &= + authentication-manager-ref? + + +after-invocation-provider = + ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. + element after-invocation-provider {ref} + +pre-post-annotation-handling = + ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. + element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} + +invocation-attribute-factory = + ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + element invocation-attribute-factory {ref} + +pre-invocation-advice = + ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. + element pre-invocation-advice {ref} + +post-invocation-advice = + ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. + element post-invocation-advice {ref} + + +expression-handler = + ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. + element expression-handler {ref} + +protect-pointcut = + ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. + element protect-pointcut {protect-pointcut.attlist, empty} +protect-pointcut.attlist &= + ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). + attribute expression {xsd:string} +protect-pointcut.attlist &= + ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" + attribute access {xsd:token} + +websocket-message-broker = + ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. + element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } + +websocket-message-broker.attrlist &= + ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. + attribute id {xsd:token}? +websocket-message-broker.attrlist &= + ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + attribute same-origin-disabled {xsd:boolean}? + +intercept-message = + ## Creates an authorization rule for a websocket message. + element intercept-message {intercept-message.attrlist} + +intercept-message.attrlist &= + ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. + attribute pattern {xsd:token}? +intercept-message.attrlist &= + ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. + attribute access {xsd:token}? +intercept-message.attrlist &= + ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? + +http-firewall = + ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. + element http-firewall {ref} + +http = + ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & saml2-login? & saml2-logout? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } +http.attlist &= + ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. + attribute pattern {xsd:token}? +http.attlist &= + ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. + attribute security {"none"}? +http.attlist &= + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref { xsd:token }? +http.attlist &= + ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. + attribute auto-config {xsd:boolean}? +http.attlist &= + use-expressions? +http.attlist &= + ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. + attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? +http.attlist &= + ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. + attribute security-context-repository-ref {xsd:token}? +http.attlist &= + ## Optional attribute that specifies that the SecurityContext should require explicit saving rather than being synchronized from the SecurityContextHolder. Defaults to "false". + attribute security-context-explicit-save {xsd:boolean}? +http.attlist &= + request-matcher? +http.attlist &= + ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". + attribute servlet-api-provision {xsd:boolean}? +http.attlist &= + ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + attribute jaas-api-provision {xsd:boolean}? +http.attlist &= + ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. + attribute access-decision-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". + attribute realm {xsd:token}? +http.attlist &= + ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + attribute entry-point-ref {xsd:token}? +http.attlist &= + ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "true" + attribute once-per-request {xsd:boolean}? +http.attlist &= + ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). + attribute disable-url-rewriting {xsd:boolean}? +http.attlist &= + ## Exposes the list of filters defined by this configuration under this bean name in the application context. + name? +http.attlist &= + authentication-manager-ref? + +access-denied-handler = + ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. + element access-denied-handler {access-denied-handler.attlist, empty} +access-denied-handler.attlist &= (ref | access-denied-handler-page) + +access-denied-handler-page = + ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. + attribute error-page {xsd:token} + +intercept-url = + ## Specifies the access attributes and/or filter list for a particular set of URLs. + element intercept-url {intercept-url.attlist, empty} +intercept-url.attlist &= + (pattern | request-matcher-ref) +intercept-url.attlist &= + ## The access configuration attributes that apply for the configured path. + attribute access {xsd:token}? +intercept-url.attlist &= + ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. + attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? + +intercept-url.attlist &= + ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. + attribute requires-channel {xsd:token}? +intercept-url.attlist &= + ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. + attribute servlet-path {xsd:token}? + +logout = + ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. + element logout {logout.attlist, empty} +logout.attlist &= + ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. + attribute logout-url {xsd:token}? +logout.attlist &= + ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). + attribute logout-success-url {xsd:token}? +logout.attlist &= + ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. + attribute invalidate-session {xsd:boolean}? +logout.attlist &= + ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. + attribute success-handler-ref {xsd:token}? +logout.attlist &= + ## A comma-separated list of the names of cookies which should be deleted when the user logs out + attribute delete-cookies {xsd:token}? + +request-cache = + ## Allow the RequestCache used for saving requests during the login process to be set + element request-cache {ref} + +form-login = + ## Sets up a form login configuration for authentication with a username and password + element form-login {form-login.attlist, empty} +form-login.attlist &= + ## The URL that the login form is posted to. If unspecified, it defaults to /login. + attribute login-processing-url {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the username. Defaults to 'username'. + attribute username-parameter {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the password. Defaults to 'password'. + attribute password-parameter {xsd:token}? +form-login.attlist &= + ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. + attribute default-target-url {xsd:token}? +form-login.attlist &= + ## Whether the user should always be redirected to the default-target-url after login. + attribute always-use-default-target {xsd:boolean}? +form-login.attlist &= + ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. + attribute login-page {xsd:token}? +form-login.attlist &= + ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. + attribute authentication-failure-url {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination + attribute authentication-success-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination + attribute authentication-failure-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationFailureHandler + attribute authentication-failure-forward-url {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationSuccessHandler + attribute authentication-success-forward-url {xsd:token}? + +oauth2-login = + ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + element oauth2-login {oauth2-login.attlist} +oauth2-login.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the GrantedAuthoritiesMapper + attribute user-authorities-mapper-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2UserService + attribute user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OpenID Connect OAuth2UserService + attribute oidc-user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +oauth2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + attribute jwt-decoder-factory-ref {xsd:token}? + +oauth2-client = + ## Configures OAuth 2.0 Client support. + element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } +oauth2-client.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? + +authorization-code-grant = + ## Configures OAuth 2.0 Authorization Code Grant. + element authorization-code-grant {authorization-code-grant.attlist, empty} +authorization-code-grant.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? + +client-registrations = + ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registrations {client-registration+, provider*} + +client-registration = + ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registration {client-registration.attlist} +client-registration.attlist &= + ## The ID that uniquely identifies the client registration. + attribute registration-id {xsd:token} +client-registration.attlist &= + ## The client identifier. + attribute client-id {xsd:token} +client-registration.attlist &= + ## The client secret. + attribute client-secret {xsd:token}? +client-registration.attlist &= + ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). + attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? +client-registration.attlist &= + ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials, password and implicit. + attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password" | "implicit"}? +client-registration.attlist &= + ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. + attribute redirect-uri {xsd:token}? +client-registration.attlist &= + ## A comma-separated list of scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. + attribute scope {xsd:token}? +client-registration.attlist &= + ## A descriptive name used for the client. The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. + attribute client-name {xsd:token}? +client-registration.attlist &= + ## A reference to the associated provider. May reference a 'provider' element or use one of the common providers (google, github, facebook, okta). + attribute provider-id {xsd:token} + +provider = + ## The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + element provider {provider.attlist} +provider.attlist &= + ## The ID that uniquely identifies the provider. + attribute provider-id {xsd:token} +provider.attlist &= + ## The Authorization Endpoint URI for the Authorization Server. + attribute authorization-uri {xsd:token}? +provider.attlist &= + ## The Token Endpoint URI for the Authorization Server. + attribute token-uri {xsd:token}? +provider.attlist &= + ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. + attribute user-info-uri {xsd:token}? +provider.attlist &= + ## The authentication method used when sending the access token to the UserInfo Endpoint. The supported values are header, form and query. + attribute user-info-authentication-method {"header" | "form" | "query"}? +provider.attlist &= + ## The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + attribute user-info-user-name-attribute {xsd:token}? +provider.attlist &= + ## The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. + attribute jwk-set-uri {xsd:token}? +provider.attlist &= + ## The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + attribute issuer-uri {xsd:token}? + +oauth2-resource-server = + ## Configures authentication support as an OAuth 2.0 Resource Server. + element oauth2-resource-server {oauth2-resource-server.attlist, (jwt? & opaque-token?)} +oauth2-resource-server.attlist &= + ## Reference to an AuthenticationManagerResolver + attribute authentication-manager-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a BearerTokenResolver + attribute bearer-token-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a AuthenticationEntryPoint + attribute entry-point-ref {xsd:token}? + +jwt = + ## Configures JWT authentication + element jwt {jwt.attlist} +jwt.attlist &= + ## The URI to use to collect the JWK Set for verifying JWTs + attribute jwk-set-uri {xsd:token}? +jwt.attlist &= + ## Reference to a JwtDecoder + attribute decoder-ref {xsd:token}? +jwt.attlist &= + ## Reference to a Converter + attribute jwt-authentication-converter-ref {xsd:token}? + +opaque-token = + ## Configuration Opaque Token authentication + element opaque-token {opaque-token.attlist} +opaque-token.attlist &= + ## The URI to use to introspect opaque token attributes + attribute introspection-uri {xsd:token}? +opaque-token.attlist &= + ## The Client ID to use to authenticate the introspection request + attribute client-id {xsd:token}? +opaque-token.attlist &= + ## The Client secret to use to authenticate the introspection request + attribute client-secret {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenIntrospector + attribute introspector-ref {xsd:token}? + +openid-login = + ## Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. + element openid-login {form-login.attlist, user-service-ref?, attribute-exchange*} + +attribute-exchange = + ## Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + element attribute-exchange {attribute-exchange.attlist, openid-attribute+} + +attribute-exchange.attlist &= + ## A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + attribute identifier-match {xsd:token}? + +openid-attribute = + ## Attributes used when making an OpenID AX Fetch Request. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. + element openid-attribute {openid-attribute.attlist} + +openid-attribute.attlist &= + ## Specifies the name of the attribute that you wish to get back. For example, email. + attribute name {xsd:token} +openid-attribute.attlist &= + ## Specifies the attribute type. For example, https://axschema.org/contact/email. See your OP's documentation for valid attribute types. + attribute type {xsd:token} +openid-attribute.attlist &= + ## Specifies if this attribute is required to the OP, but does not error out if the OP does not return the attribute. Default is false. + attribute required {xsd:boolean}? +openid-attribute.attlist &= + ## Specifies the number of attributes that you wish to get back. For example, return 3 emails. The default value is 1. + attribute count {xsd:int}? + +saml2-login = + ## Configures authentication support for SAML 2.0 Login + element saml2-login {saml2-login.attlist} +saml2-login.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestRepository + attribute authentication-request-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestResolver + attribute authentication-request-resolver-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationConverter + attribute authentication-converter-ref {xsd:token}? +saml2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +saml2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationManager + attribute authentication-manager-ref {xsd:token}? + +saml2-logout = + ## Configures SAML 2.0 Single Logout support + element saml2-logout {saml2-logout.attlist} +saml2-logout.attlist &= + ## The URL by which the relying or asserting party can trigger logout + attribute logout-url {xsd:token}? +saml2-logout.attlist &= + ## The URL by which the asserting party can send a SAML 2.0 Logout Request + attribute logout-request-url {xsd:token}? +saml2-logout.attlist &= + ## The URL by which the asserting party can send a SAML 2.0 Logout Response + attribute logout-response-url {xsd:token}? +saml2-logout.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestValidator + attribute logout-request-validator-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestResolver + attribute logout-request-resolver-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestRepository + attribute logout-request-repository-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutResponseValidator + attribute logout-response-validator-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutResponseResolver + attribute logout-response-resolver-ref {xsd:token}? + +relying-party-registrations = + ## Container element for relying party(ies) registered with a SAML 2.0 identity provider + element relying-party-registrations {relying-party-registration+, asserting-party*} + +relying-party-registration = + ## Represents a relying party registered with a SAML 2.0 identity provider + element relying-party-registration {relying-party-registration.attlist, signing-credential*, decryption-credential*} +relying-party-registration.attlist &= + ## The ID that uniquely identifies the relying party registration. + attribute registration-id {xsd:token} +relying-party-registration.attlist &= + ## The location of the Identity Provider's metadata. + attribute metadata-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party's EntityID + attribute entity-id {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Location + attribute assertion-consumer-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Binding + attribute assertion-consumer-service-binding {xsd:token}? +relying-party-registration.attlist &= + ## A reference to the associated asserting party. + attribute asserting-party-id {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? + +signing-credential = + ## The relying party's signing credential + element signing-credential {signing-credential.attlist} +signing-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +signing-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +decryption-credential = + ## The relying party's decryption credential + element decryption-credential {decryption-credential.attlist} +decryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +decryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +asserting-party = + ## The configuration metadata of the Asserting party + element asserting-party {asserting-party.attlist, verification-credential*, encryption-credential*} +asserting-party.attlist &= + ## A unique identifier of the asserting party. + attribute asserting-party-id {xsd:token} +asserting-party.attlist &= + ## The asserting party's EntityID. + attribute entity-id {xsd:token} +asserting-party.attlist &= + ## Indicates the asserting party's preference that relying parties should sign the AuthnRequest before sending + attribute want-authn-requests-signed {xsd:token}? +asserting-party.attlist &= + ## The SingleSignOnService Location. + attribute single-sign-on-service-location {xsd:token} +asserting-party.attlist &= + ## The SingleSignOnService Binding. + attribute single-sign-on-service-binding {xsd:token}? +asserting-party.attlist &= + ## A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this asserting party, in preference order. + attribute signing-algorithms {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? + +verification-credential = + ## The relying party's verification credential + element verification-credential {verification-credential.attlist} +verification-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +verification-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +encryption-credential = + ## The asserting party's encryption credential + element encryption-credential {encryption-credential.attlist} +encryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +encryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +filter-chain-map = + ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + element filter-chain-map {filter-chain-map.attlist, filter-chain+} +filter-chain-map.attlist &= + request-matcher? + +filter-chain = + ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. + element filter-chain {filter-chain.attlist, empty} +filter-chain.attlist &= + (pattern | request-matcher-ref) +filter-chain.attlist &= + ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + attribute filters {xsd:token} + +pattern = + ## The request URL pattern which will be mapped to the FilterChain. + attribute pattern {xsd:token} +request-matcher-ref = + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref {xsd:token} + +filter-security-metadata-source = + ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. + element filter-security-metadata-source {fsmds.attlist, intercept-url+} +fsmds.attlist &= + use-expressions? +fsmds.attlist &= + id? +fsmds.attlist &= + request-matcher? + +http-basic = + ## Adds support for basic authentication + element http-basic {http-basic.attlist, empty} + +http-basic.attlist &= + ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + attribute entry-point-ref {xsd:token}? +http-basic.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +password-management = + ## Adds support for the password management. + element password-management {password-management.attlist, empty} + +password-management.attlist &= + ## The change password page. Defaults to "/change-password". + attribute change-password-page {xsd:string}? + +session-management = + ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. + element session-management {session-management.attlist, concurrency-control?} + +session-management.attlist &= + ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. + attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? +session-management.attlist &= + ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. + attribute invalid-session-url {xsd:token}? +session-management.attlist &= + ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter + attribute invalid-session-strategy-ref {xsd:token}? +session-management.attlist &= + ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter + attribute session-authentication-strategy-ref {xsd:token}? +session-management.attlist &= + ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. + attribute session-authentication-error-url {xsd:token}? + + +concurrency-control = + ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. + element concurrency-control {concurrency-control.attlist, empty} + +concurrency-control.attlist &= + ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. + attribute max-sessions {xsd:token}? +concurrency-control.attlist &= + ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. + attribute expired-url {xsd:token}? +concurrency-control.attlist &= + ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter + attribute expired-session-strategy-ref {xsd:token}? +concurrency-control.attlist &= + ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. + attribute error-if-maximum-exceeded {xsd:boolean}? +concurrency-control.attlist &= + ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. + attribute session-registry-alias {xsd:token}? +concurrency-control.attlist &= + ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. + attribute session-registry-ref {xsd:token}? + + +remember-me = + ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. + element remember-me {remember-me.attlist} +remember-me.attlist &= + ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? + +remember-me.attlist &= + (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) + +remember-me.attlist &= + user-service-ref? + +remember-me.attlist &= + ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. + attribute services-alias {xsd:token}? + +remember-me.attlist &= + ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. + attribute use-secure-cookie {xsd:boolean}? + +remember-me.attlist &= + ## The period (in seconds) for which the remember-me cookie should be valid. + attribute token-validity-seconds {xsd:string}? + +remember-me.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. + attribute authentication-success-handler-ref {xsd:token}? +remember-me.attlist &= + ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-parameter {xsd:token}? +remember-me.attlist &= + ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-cookie {xsd:token}? + +token-repository-ref = + ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. + attribute token-repository-ref {xsd:token} +remember-me-services-ref = + ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. + attribute services-ref {xsd:token}? +remember-me-data-source-ref = + ## DataSource bean for the database that contains the token repository schema. + data-source-ref + +anonymous = + ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. + element anonymous {anonymous.attlist} +anonymous.attlist &= + ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? +anonymous.attlist &= + ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". + attribute username {xsd:token}? +anonymous.attlist &= + ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + attribute granted-authority {xsd:token}? +anonymous.attlist &= + ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. + attribute enabled {xsd:boolean}? + + +port-mappings = + ## Defines the list of mappings between http and https ports for use in redirects + element port-mappings {port-mappings.attlist, port-mapping+} + +port-mappings.attlist &= empty + +port-mapping = + ## Provides a method to map http ports to https ports when forcing a redirect. + element port-mapping {http-port, https-port} + +http-port = + ## The http port to use. + attribute http {xsd:token} + +https-port = + ## The https port to use. + attribute https {xsd:token} + + +x509 = + ## Adds support for X.509 client authentication. + element x509 {x509.attlist} +x509.attlist &= + ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". + attribute subject-principal-regex {xsd:token}? +x509.attlist &= + ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. + user-service-ref? +x509.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +jee = + ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. + element jee {jee.attlist} +jee.attlist &= + ## A comma-separate list of roles to look for in the incoming HttpServletRequest. + attribute mappable-roles {xsd:token} +jee.attlist &= + ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. + user-service-ref? + +authentication-manager = + ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. + element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} +authman.attlist &= + id? +authman.attlist &= + ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) + attribute alias {xsd:token}? +authman.attlist &= + ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. + attribute erase-credentials {xsd:boolean}? + +authentication-provider = + ## Indicates that the contained user-service should be used as an authentication source. + element authentication-provider {ap.attlist & any-user-service & password-encoder?} +ap.attlist &= + ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. + ref? +ap.attlist &= + ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. + user-service-ref? + +user-service = + ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + element user-service {id? & (properties-file | (user*))} +properties-file = + ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + attribute properties {xsd:token}? + +user = + ## Represents a user in the application. + element user {user.attlist, empty} +user.attlist &= + ## The username assigned to the user. + attribute name {xsd:token} +user.attlist &= + ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. + attribute password {xsd:string}? +user.attlist &= + ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + attribute authorities {xsd:token} +user.attlist &= + ## Can be set to "true" to mark an account as locked and unusable. + attribute locked {xsd:boolean}? +user.attlist &= + ## Can be set to "true" to mark an account as disabled and unusable. + attribute disabled {xsd:boolean}? + +jdbc-user-service = + ## Causes creation of a JDBC-based UserDetailsService. + element jdbc-user-service {id? & jdbc-user-service.attlist} +jdbc-user-service.attlist &= + ## The bean ID of the DataSource which provides the required tables. + attribute data-source-ref {xsd:token} +jdbc-user-service.attlist &= + cache-ref? +jdbc-user-service.attlist &= + ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" + attribute users-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" + attribute authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + attribute group-authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + role-prefix? + +csrf = +## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. + element csrf {csrf-options.attlist} +csrf-options.attlist &= + ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). + attribute disabled {xsd:boolean}? +csrf-options.attlist &= + ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + attribute request-matcher-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository. + attribute token-repository-ref { xsd:token }? + +headers = +## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & cross-origin-opener-policy? & cross-origin-embedder-policy? & cross-origin-resource-policy? & header*)} +headers-options.attlist &= + ## Specifies if the default headers should be disabled. Default false. + attribute defaults-disabled {xsd:token}? +headers-options.attlist &= + ## Specifies if headers should be disabled. Default false. + attribute disabled {xsd:token}? +hsts = + ## Adds support for HTTP Strict Transport Security (HSTS) + element hsts {hsts-options.attlist} +hsts-options.attlist &= + ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hsts-options.attlist &= + ## Specifies if subdomains should be included. Default true. + attribute include-subdomains {xsd:boolean}? +hsts-options.attlist &= + ## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year. + attribute max-age-seconds {xsd:integer}? +hsts-options.attlist &= + ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. + attribute request-matcher-ref { xsd:token }? +hsts-options.attlist &= + ## Specifies if preload should be included. Default false. + attribute preload {xsd:boolean}? + +cors = +## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource +element cors { cors-options.attlist } +cors-options.attlist &= + ref? +cors-options.attlist &= + ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use + attribute configuration-source-ref {xsd:token}? + +hpkp = + ## Adds support for HTTP Public Key Pinning (HPKP). + element hpkp {hpkp.pins,hpkp.attlist} +hpkp.pins = + ## The list with pins + element pins {hpkp.pin+} +hpkp.pin = + ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute + element pin { + ## The cryptographic hash algorithm + attribute algorithm { xsd:string }?, + text + } +hpkp.attlist &= + ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hpkp.attlist &= + ## Specifies if subdomains should be included. Default false. + attribute include-subdomains {xsd:boolean}? +hpkp.attlist &= + ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + attribute max-age-seconds {xsd:integer}? +hpkp.attlist &= + ## Specifies if the browser should only report pin validation failures. Default true. + attribute report-only {xsd:boolean}? +hpkp.attlist &= + ## Specifies the URI to which the browser should report pin validation failures. + attribute report-uri {xsd:string}? + +content-security-policy = + ## Adds support for Content Security Policy (CSP) + element content-security-policy {csp-options.attlist} +csp-options.attlist &= + ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + attribute policy-directives {xsd:token}? +csp-options.attlist &= + ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + attribute report-only {xsd:boolean}? + +referrer-policy = + ## Adds support for Referrer Policy + element referrer-policy {referrer-options.attlist} +referrer-options.attlist &= + ## The policies for the Referrer-Policy header. + attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? + +feature-policy = + ## Adds support for Feature Policy + element feature-policy {feature-options.attlist} +feature-options.attlist &= + ## The security policy directive(s) for the Feature-Policy header. + attribute policy-directives {xsd:token}? + +permissions-policy = + ## Adds support for Permissions Policy + element permissions-policy {permissions-options.attlist} +permissions-options.attlist &= + ## The policies for the Permissions-Policy header. + attribute policy {xsd:token}? + +cache-control = + ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request + element cache-control {cache-control.attlist} +cache-control.attlist &= + ## Specifies if Cache Control should be disabled. Default false. + attribute disabled {xsd:boolean}? + +frame-options = + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} +frame-options.attlist &= + ## If disabled, the X-Frame-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? +frame-options.attlist &= + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? +frame-options.attlist &= + ## Specify the strategy to use when ALLOW-FROM is chosen. + attribute strategy {"static","whitelist","regexp"}? +frame-options.attlist &= + ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. + ref? +frame-options.attlist &= + ## Specify a value to use for the chosen strategy. + attribute value {xsd:string}? +frame-options.attlist &= + ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. + ## Deprecated ALLOW-FROM is an obsolete directive that no longer works in modern browsers. Instead use + ## Content-Security-Policy with the + ## frame-ancestors + ## directive. + attribute from-parameter {xsd:string}? + + +xss-protection = + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} +xss-protection.attlist &= + ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + attribute disabled {xsd:boolean}? +xss-protection.attlist &= + ## specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? +xss-protection.attlist &= + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? + +content-type-options = + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {content-type-options.attlist, empty} +content-type-options.attlist &= + ## If disabled, the X-Content-Type-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? + +cross-origin-opener-policy = + ## Adds support for Cross-Origin-Opener-Policy header + element cross-origin-opener-policy {cross-origin-opener-policy-options.attlist,empty} +cross-origin-opener-policy-options.attlist &= + ## The policies for the Cross-Origin-Opener-Policy header. + attribute policy {"unsafe-none","same-origin","same-origin-allow-popups"}? + +cross-origin-embedder-policy = + ## Adds support for Cross-Origin-Embedder-Policy header + element cross-origin-embedder-policy {cross-origin-embedder-policy-options.attlist,empty} +cross-origin-embedder-policy-options.attlist &= + ## The policies for the Cross-Origin-Embedder-Policy header. + attribute policy {"unsafe-none","require-corp"}? + +cross-origin-resource-policy = + ## Adds support for Cross-Origin-Resource-Policy header + element cross-origin-resource-policy {cross-origin-resource-policy-options.attlist,empty} +cross-origin-resource-policy-options.attlist &= + ## The policies for the Cross-Origin-Resource-Policy header. + attribute policy {"cross-origin","same-origin","same-site"}? + +header= + ## Add additional headers to the response. + element header {header.attlist} +header.attlist &= + ## The name of the header to add. + attribute name {xsd:token}? +header.attlist &= + ## The value for the header. + attribute value {xsd:token}? +header.attlist &= + ## Reference to a custom HeaderWriter implementation. + ref? + +any-user-service = user-service | jdbc-user-service | ldap-user-service + +custom-filter = + ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. + element custom-filter {custom-filter.attlist} + +custom-filter.attlist &= + ref + +custom-filter.attlist &= + (after | before | position) + +after = + ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. + attribute after {named-security-filter} +before = + ## The filter immediately before which the custom-filter should be placed in the chain + attribute before {named-security-filter} +position = + ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. + attribute position {named-security-filter} + +named-security-filter = "FIRST" | "DISABLE_ENCODE_URL_FILTER" | "FORCE_EAGER_SESSION_FILTER" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "SAML2_LOGOUT_REQUEST_FILTER" | "SAML2_LOGOUT_RESPONSE_FILTER" | "CSRF_FILTER" | "SAML2_LOGOUT_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd new file mode 100644 index 00000000000..fb3271c2cc4 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd @@ -0,0 +1,3764 @@ + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + Whether a string should be base64 encoded + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + + + Specifies a URL. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + A reference to an AuthenticationManager bean + + + + + + + + A reference to a DataSource bean + + + + + + + Enables Spring Security debugging infrastructure. This will provide human-readable + (multi-line) debugging information to monitor requests coming into the security filters. + This may include sensitive information, such as request parameters or headers, and should + only be used in a development environment. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Defines an LDAP server location or starts an embedded server. The url indicates the + location of a remote server. If no url is given, an embedded server will be started, + listening on the supplied port number. The port is optional and defaults to 33389. A + Spring LDAP ContextSource bean will be registered for the server with the id supplied. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Specifies a URL. + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + Username (DN) of the "manager" user identity which will be used to authenticate to a + (non-embedded) LDAP server. If omitted, anonymous access will be used. + + + + + + The password for the manager DN. This is required if the manager-dn is specified. + + + + + + Explicitly specifies an ldif file resource to load into an embedded LDAP server. The + default is classpath*:*.ldiff + + + + + + Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + + + + + + Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and + 'unboundid'. By default, it will depends if the library is available in the classpath. + + + + + + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + This element configures a LdapUserDetailsService which is a combination of a + FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key + "{0}" must be present and will be substituted with the username. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The attribute in the directory which contains the user password. Defaults to + "userPassword". + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + Can be used inside a bean definition to add a security interceptor to the bean and set up + access configuration attributes for the bean's methods + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + Optional AccessDecisionManager bean ID to be used by the created method security + interceptor. + + + + + + + + + A method name + + + + + + Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + + + + + + + Creates a MethodSecurityMetadataSource instance + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with Spring Security annotations. Where + there is a match, the beans will automatically be proxied and security authorization + applied to the methods accordingly. Interceptors are invoked in the order specified in + AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "true". + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "false". + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "false". + + + + + + If true, class-based proxying will be used instead of interface-based proxying. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with the ordered list of + "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a + match, the beans will automatically be proxied and security authorization applied to the + methods accordingly. If you use and enable all four sources of method security metadata + (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 + security annotations), the metadata sources will be queried in that order. In practical + terms, this enables you to use XML to override method security metadata expressed in + annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize + etc.), @Secured and finally JSR-250. + + + + + + + + Allows the default expression-based mechanism for handling Spring Security's pre and post + invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be + replace entirely. Only applies if these annotations are enabled. + + + + + + + Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and + post invocation metadata from the annotated methods. + + + + + + + + + Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the + PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. + + + + + + + + + Customizes the PostInvocationAdviceProvider with the ref as the + PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + + + + Allows addition of extra AfterInvocationProvider beans which should be called by the + MethodSecurityInterceptor created by global-method-security. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "disabled". + + + + + + + + + + + + Optional AccessDecisionManager bean ID to override the default used for method security. + + + + + + Optional RunAsmanager implementation which will be used by the configured + MethodSecurityInterceptor + + + + + + Allows the advice "order" to be set for the method security interceptor. + + + + + + If true, class based proxying will be used instead of interface based proxying. + + + + + + Can be used to specify that AspectJ should be used instead of the default Spring AOP. If + set, secured classes must be woven with the AnnotationSecurityAspect from the + spring-security-aspects module. + + + + + + + + + + + An external MethodSecurityMetadataSource instance can be supplied which will take priority + over other sources (such as the default annotations). + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + + + + + + + An AspectJ expression, including the 'execution' keyword. For example, 'execution(int + com.foo.TargetObject.countLength(String))' (without the quotes). + + + + + + Access configuration attributes list that applies to all methods matching the pointcut, + e.g. "ROLE_A,ROLE_B" + + + + + + + Allows securing a Message Broker. There are two modes. If no id is specified: ensures that + any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver + registered as a custom argument resolver; ensures that the + SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that + can be manually registered with the clientInboundChannel. + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. If specified, + explicit configuration within clientInboundChannel is required. If not specified, ensures + that any SimpAnnotationMethodMessageHandler has the + AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures + that the SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. + + + + + + Disables the requirement for CSRF token to be present in the Stomp headers (default + false). Changing the default is useful if it is necessary to allow other origins to make + SockJS connections. + + + + + + + Creates an authorization rule for a websocket message. + + + + + + + + + + The destination ant pattern which will be mapped to the access attribute. For example, /** + matches any message with a destination, /admin/** matches any message that has a + destination that starts with admin. + + + + + + The access configuration attributes that apply for the configured message. For example, + permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role + 'ROLE_ADMIN'. + + + + + + The type of message to match on. Valid values are defined in SimpMessageType (i.e. + CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, + DISCONNECT_ACK, OTHER). + + + + + + + + + + + + + + + + + + + + Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created + by the namespace. + + + + + + + + + Container element for HTTP security configuration. Multiple elements can now be defined, + each with a specific pattern to which the enclosed security configuration applies. A + pattern can also be configured to bypass Spring Security's filters completely by setting + the "security" attribute to "none". + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + Defines the access-denied strategy that should be used. An access denied page can be + defined or a reference to an AccessDeniedHandler instance. + + + + + + + + + Sets up a form login configuration for authentication with a username and password + + + + + + + + + + + + Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and + 2.0 protocols have been deprecated and users are <a + href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to + migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is + supported by <code>spring-security-oauth2</code>. + + + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + Configures authentication support for SAML 2.0 Login + + + + + + + + + Configures SAML 2.0 Single Logout support + + + + + + + + + Adds support for X.509 client authentication. + + + + + + + + + + Adds support for basic authentication + + + + + + + + + Incorporates a logout processing filter. Most web applications require a logout filter, + although you may not require one if you write a controller to provider similar logic. + + + + + + + + + + Session-management related functionality is implemented by the addition of a + SessionManagementFilter to the filter stack. + + + + + + + Enables concurrent session control, limiting the number of authenticated sessions a user + may have at the same time. + + + + + + + + + + + + + Sets up remember-me authentication. If used with the "key" attribute (or no attributes) + the cookie-only implementation will be used. Specifying "token-repository-ref" or + "remember-me-data-source-ref" will use the more secure, persisten token approach. + + + + + + + + + Adds support for automatically granting all anonymous web requests a particular principal + identity and a corresponding granted authority. + + + + + + + + + Defines the list of mappings between http and https ports for use in redirects + + + + + + + Provides a method to map http ports to https ports when forcing a redirect. + + + + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + + + + The request URL pattern which will be mapped to the filter chain created by this <http> + element. If omitted, the filter chain will match all requests. + + + + + + When set to 'none', requests matching the pattern attribute will be ignored by Spring + Security. No security filters will be applied and no SecurityContext will be available. If + set, the <http> element must be empty, with no children. + + + + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A legacy attribute which automatically registers a login form, BASIC authentication and a + logout URL and logout services. If unspecified, defaults to "false". We'd recommend you + avoid using this and instead explicitly configure the services you require. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + Controls the eagerness with which an HTTP session is created by Spring Security classes. + If not set, defaults to "ifRequired". If "stateless" is used, this implies that the + application guarantees that it will not create a session. This differs from the use of + "never" which means that Spring Security will not create a session, but will make use of + one if the application does. + + + + + + + + + + + + + + A reference to a SecurityContextRepository bean. This can be used to customize how the + SecurityContext is stored between requests. + + + + + + Optional attribute that specifies that the SecurityContext should require explicit saving + rather than being synchronized from the SecurityContextHolder. Defaults to "false". + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + Provides versions of HttpServletRequest security methods such as isUserInRole() and + getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to + "true". + + + + + + If available, runs the request as the Subject acquired from the JaasAuthenticationToken. + Defaults to "false". + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the realm name that will be used for all authentication + features that require a realm name (eg BASIC and Digest authentication). If unspecified, + defaults to "Spring Security Application". + + + + + + Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + + + + + + Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults + to "true" + + + + + + Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" + (rewriting is disabled). + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + The access configuration attributes that apply for the configured path. + + + + + + The HTTP Method for which the access configuration attributes should apply. If not + specified, the attributes will apply to any method. + + + + + + + + + + + + + + + + + + Used to specify that a URL must be accessed over http or https, or that there is no + preference. The value should be "http", "https" or "any", respectively. + + + + + + The path to the servlet. This attribute is only applicable when 'request-matcher' is + 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are + 2 or more HttpServlet's registered in the ServletContext that have mappings starting with + '/' and are different; 2) The pattern starts with the same value of a registered + HttpServlet path, excluding the default (root) HttpServlet '/'. + + + + + + + + + Specifies the URL that will cause a logout. Spring Security will initialize a filter that + responds to this particular URL. Defaults to /logout if unspecified. + + + + + + Specifies the URL to display once the user has logged out. If not specified, defaults to + <form-login-login-page>/?logout (i.e. /login?logout). + + + + + + Specifies whether a logout also causes HttpSession invalidation, which is generally + desirable. If unspecified, defaults to true. + + + + + + A reference to a LogoutSuccessHandler implementation which will be used to determine the + destination to which the user is taken after logging out. + + + + + + A comma-separated list of the names of cookies which should be deleted when the user logs + out + + + + + + + Allow the RequestCache used for saving requests during the login process to be set + + + + + + + + + + + The URL that the login form is posted to. If unspecified, it defaults to /login. + + + + + + The name of the request parameter which contains the username. Defaults to 'username'. + + + + + + The name of the request parameter which contains the password. Defaults to 'password'. + + + + + + The URL that will be redirected to after successful authentication, if the user's previous + action could not be resumed. This generally happens if the user visits a login page + without having first requested a secured operation that triggers authentication. If + unspecified, defaults to the root of the application. + + + + + + Whether the user should always be redirected to the default-target-url after login. + + + + + + The URL for the login page. If no login URL is specified, Spring Security will + automatically create a login URL at GET /login and a corresponding filter to render that + login URL when requested. + + + + + + The URL for the login failure page. If no login failure URL is specified, Spring Security + will automatically create a failure login URL at /login?error and a corresponding filter + to render that login failure URL when requested. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful authentication request. Should not be used in combination with + default-target-url (or always-use-default-target-url) as the implementation should always + deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationFailureHandler bean which should be used to handle a failed + authentication request. Should not be used in combination with authentication-failure-url + as the implementation should always deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + The URL for the ForwardAuthenticationFailureHandler + + + + + + The URL for the ForwardAuthenticationSuccessHandler + + + + + + + Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + Reference to the GrantedAuthoritiesMapper + + + + + + Reference to the OAuth2UserService + + + + + + Reference to the OpenID Connect OAuth2UserService + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + + + + + + + Configures OAuth 2.0 Client support. + + + + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + + Configures OAuth 2.0 Authorization Code Grant. + + + + + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + + Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 + Provider. + + + + + + + + + + + + Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the client registration. + + + + + + The client identifier. + + + + + + The client secret. + + + + + + The method used to authenticate the client with the provider. The supported values are + client_secret_basic, client_secret_post and none (public clients). + + + + + + + + + + + + + + + The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The + supported values are authorization_code, client_credentials, password and implicit. + + + + + + + + + + + + + + The client’s registered redirect URI that the Authorization Server redirects the + end-user’s user-agent to after the end-user has authenticated and authorized access to the + client. + + + + + + A comma-separated list of scope(s) requested by the client during the Authorization + Request flow, such as openid, email, or profile. + + + + + + A descriptive name used for the client. The name may be used in certain scenarios, such as + when displaying the name of the client in the auto-generated login page. + + + + + + A reference to the associated provider. May reference a 'provider' element or use one of + the common providers (google, github, facebook, okta). + + + + + + + The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the provider. + + + + + + The Authorization Endpoint URI for the Authorization Server. + + + + + + The Token Endpoint URI for the Authorization Server. + + + + + + The UserInfo Endpoint URI used to access the claims/attributes of the authenticated + end-user. + + + + + + The authentication method used when sending the access token to the UserInfo Endpoint. The + supported values are header, form and query. + + + + + + + + + + + + + The name of the attribute returned in the UserInfo Response that references the Name or + Identifier of the end-user. + + + + + + The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which + contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID + Token and optionally the UserInfo Response. + + + + + + The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect + 1.0 Provider. + + + + + + + Configures authentication support as an OAuth 2.0 Resource Server. + + + + + + + + + + + + + + Reference to an AuthenticationManagerResolver + + + + + + Reference to a BearerTokenResolver + + + + + + Reference to a AuthenticationEntryPoint + + + + + + + Configures JWT authentication + + + + + + + + + + The URI to use to collect the JWK Set for verifying JWTs + + + + + + Reference to a JwtDecoder + + + + + + Reference to a Converter<Jwt, AbstractAuthenticationToken> + + + + + + + Configuration Opaque Token authentication + + + + + + + + + + The URI to use to introspect opaque token attributes + + + + + + The Client ID to use to authenticate the introspection request + + + + + + The Client secret to use to authenticate the introspection request + + + + + + Reference to an OpaqueTokenIntrospector + + + + + + + + Sets up an attribute exchange configuration to request specified attributes from the + OpenID identity provider. When multiple elements are used, each must have an + identifier-attribute attribute. Each configuration will be matched in turn against the + supplied login identifier until a match is found. + + + + + + + + + + + + + A regular expression which will be compared against the claimed identity, when deciding + which attribute-exchange configuration to use during authentication. + + + + + + + Attributes used when making an OpenID AX Fetch Request. NOTE: The OpenID 1.0 and 2.0 + protocols have been deprecated and users are <a + href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to + migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is + supported by <code>spring-security-oauth2</code>. + + + + + + + + + + Specifies the name of the attribute that you wish to get back. For example, email. + + + + + + Specifies the attribute type. For example, https://axschema.org/contact/email. See your + OP's documentation for valid attribute types. + + + + + + Specifies if this attribute is required to the OP, but does not error out if the OP does + not return the attribute. Default is false. + + + + + + Specifies the number of attributes that you wish to get back. For example, return 3 + emails. The default value is 1. + + + + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2AuthenticationRequestRepository + + + + + + Reference to the Saml2AuthenticationRequestResolver + + + + + + Reference to the AuthenticationConverter + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the AuthenticationManager + + + + + + + + + The URL by which the relying or asserting party can trigger logout + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Request + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Response + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2LogoutRequestValidator + + + + + + Reference to the Saml2LogoutRequestResolver + + + + + + Reference to the Saml2LogoutRequestRepository + + + + + + Reference to the Saml2LogoutResponseValidator + + + + + + Reference to the Saml2LogoutResponseResolver + + + + + + + Container element for relying party(ies) registered with a SAML 2.0 identity provider + + + + + + + + + + + + Represents a relying party registered with a SAML 2.0 identity provider + + + + + + + + + + + + + + The ID that uniquely identifies the relying party registration. + + + + + + The location of the Identity Provider's metadata. + + + + + + The relying party's EntityID + + + + + + The Assertion Consumer Service Location + + + + + + The Assertion Consumer Service Binding + + + + + + A reference to the associated asserting party. + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Location</a> + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Response Location</a> + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Binding</a> + + + + + + + The relying party's signing credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The relying party's decryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The configuration metadata of the Asserting party + + + + + + + + + + + + + + A unique identifier of the asserting party. + + + + + + The asserting party's EntityID. + + + + + + Indicates the asserting party's preference that relying parties should sign the + AuthnRequest before sending + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Location. + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Binding. + + + + + + A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this + asserting party, in preference order. + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Location</a> + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Response Location</a> + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Binding</a> + + + + + + + The relying party's verification credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The asserting party's encryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + + + + + + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + Used within to define a specific URL pattern and the list of filters which apply to the + URLs matching that pattern. When multiple filter-chain elements are assembled in a list in + order to configure a FilterChainProxy, the most specific patterns must be placed at the + top of the list, with most general ones at the bottom. + + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A comma separated list of bean names that implement Filter that should be processed for + this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + + Used to explicitly configure a FilterSecurityMetadataSource bean for use with a + FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy + explicitly, rather than using the <http> element. The intercept-url elements used should + only contain pattern, method and access attributes. Any others will result in a + configuration error. + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + + Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds support for the password management. + + + + + + + + + + The change password page. Defaults to "/change-password". + + + + + + + + + Indicates how session fixation protection will be applied when a user authenticates. If + set to "none", no protection will be applied. "newSession" will create a new empty + session, with only Spring Security-related attributes migrated. "migrateSession" will + create a new session and copy all session attributes to the new session. In Servlet 3.1 + (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing + session and use the container-supplied session fixation protection + (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and + newer containers, "migrateSession" in older containers. Throws an exception if + "changeSessionId" is used in older containers. + + + + + + + + + + + + + + The URL to which a user will be redirected if they submit an invalid session indentifier. + Typically used to detect session timeouts. + + + + + + Allows injection of the InvalidSessionStrategy instance used by the + SessionManagementFilter + + + + + + Allows injection of the SessionAuthenticationStrategy instance used by the + SessionManagementFilter + + + + + + Defines the URL of the error page which should be shown when the + SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error + code will be returned to the client. Note that this attribute doesn't apply if the error + occurs during a form-based login, where the URL for authentication failure will take + precedence. + + + + + + + + + The maximum number of sessions a single authenticated user can have open at the same time. + Defaults to "1". A negative value denotes unlimited sessions. + + + + + + The URL a user will be redirected to if they attempt to use a session which has been + "expired" because they have logged in again. + + + + + + Allows injection of the SessionInformationExpiredStrategy instance used by the + ConcurrentSessionFilter + + + + + + Specifies that an unauthorized error should be reported when a user attempts to login when + they already have the maximum configured sessions open. The default behaviour is to expire + the original session. If the session-authentication-error-url attribute is set on the + session-management URL, the user will be redirected to this URL. + + + + + + Allows you to define an alias for the SessionRegistry bean in order to access it in your + own configuration. + + + + + + Allows you to define an external SessionRegistry bean to be used by the concurrency + control setup. + + + + + + + + + The "key" used to identify cookies from a specific token-based remember-me application. + You should set this to a unique value for your application. If unset, it will default to a + random value generated by SecureRandom. + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + A reference to a DataSource bean + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Exports the internally defined RememberMeServices as a bean alias, allowing it to be used + by other beans in the application context. + + + + + + Determines whether the "secure" flag will be set on the remember-me cookie. If set to + true, the cookie will only be submitted over HTTPS (recommended). By default, secure + cookies will be used if the request is made on a secure connection. + + + + + + The period (in seconds) for which the remember-me cookie should be valid. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful remember-me authentication. + + + + + + The name of the request parameter which toggles remember-me authentication. Defaults to + 'remember-me'. + + + + + + The name of cookie which store the token for remember-me authentication. Defaults to + 'remember-me'. + + + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + + + Allows a custom implementation of RememberMeServices to be used. Note that this + implementation should return RememberMeAuthenticationToken instances with the same "key" + value as specified in the remember-me element. Alternatively it should register its own + AuthenticationProvider. It should also implement the LogoutHandler interface, which will + be invoked when a user logs out. Typically the remember-me cookie would be removed on + logout. + + + + + + + + + + + + The key shared between the provider and filter. This generally does not need to be set. If + unset, it will default to a random value generated by SecureRandom. + + + + + + The username that should be assigned to the anonymous request. This allows the principal + to be identified, which may be important for logging and auditing. if unset, defaults to + "anonymousUser". + + + + + + The granted authority that should be assigned to the anonymous request. Commonly this is + used to assign the anonymous request particular roles, which can subsequently be used in + authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + + + + + + With the default namespace setup, the anonymous "authentication" facility is automatically + enabled. You can disable it using this property. + + + + + + + + + + The http port to use. + + + + + + + + The https port to use. + + + + + + + + + The regular expression used to obtain the username from the certificate's subject. + Defaults to matching on the common name using the pattern "CN=(.*?),". + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration + with container authentication. + + + + + + + + + + A comma-separate list of roles to look for in the incoming HttpServletRequest. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Registers the AuthenticationManager instance and allows its list of + AuthenticationProviders to be defined. Also allows you to define an alias to allow you to + reference the AuthenticationManager in your own beans. + + + + + + + Indicates that the contained user-service should be used as an authentication source. + + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + Sets up an ldap authentication provider + + + + + + + Specifies that an LDAP provider should use an LDAP compare operation of the user's + password to authenticate the user + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + An alias you wish to use for the AuthenticationManager bean (not required it you are using + a specific id) + + + + + + If set to true, the AuthenticationManger will attempt to clear any credentials data in the + returned Authentication object, once the user has been authenticated. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Creates an in-memory UserDetailsService from a properties file or a list of "user" child + elements. Usernames are converted to lower-case internally to allow for case-insensitive + lookups, so this should not be used if case-sensitivity is required. + + + + + + + Represents a user in the application. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The location of a Properties file where each line is in the format of + username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + + + + + + + + + The username assigned to the user. + + + + + + The password assigned to the user. This may be hashed if the corresponding authentication + provider supports hashing (remember to set the "hash" attribute of the "user-service" + element). This attribute be omitted in the case where the data will not be used for + authentication, but only for accessing authorities. If omitted, the namespace will + generate a random value, preventing its accidental use for authentication. Cannot be + empty. + + + + + + One of more authorities granted to the user. Separate authorities with a comma (but no + space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + + + + + Can be set to "true" to mark an account as locked and unusable. + + + + + + Can be set to "true" to mark an account as disabled and unusable. + + + + + + + Causes creation of a JDBC-based UserDetailsService. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The bean ID of the DataSource which provides the required tables. + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + An SQL statement to query a username, password, and enabled status given a username. + Default is "select username,password,enabled from users where username = ?" + + + + + + An SQL statement to query for a user's granted authorities given a username. The default + is "select username, authority from authorities where username = ?" + + + + + + An SQL statement to query user's group authorities given a username. The default is + "select g.id, g.group_name, ga.authority from groups g, group_members gm, + group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + Element for configuration of the CsrfFilter for protection against CSRF. It also updates + the default RequestCache to only replay "GET" requests. + + + + + + + + + + Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is + enabled). + + + + + + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is + any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + + + + + + The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by + LazyCsrfTokenRepository. + + + + + + + Element for configuration of the HeaderWritersFilter. Enables easy setting for the + X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies if the default headers should be disabled. Default false. + + + + + + Specifies if headers should be disabled. Default false. + + + + + + + Adds support for HTTP Strict Transport Security (HSTS) + + + + + + + + + + Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default true. + + + + + + Specifies the maximum amount of time the host should be considered a Known HSTS Host. + Default one year. + + + + + + The RequestMatcher instance to be used to determine if the header should be set. Default + is if HttpServletRequest.isSecure() is true. + + + + + + Specifies if preload should be included. Default false. + + + + + + + Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is + specified a HandlerMappingIntrospector is used as the CorsConfigurationSource + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to + use + + + + + + + Adds support for HTTP Public Key Pinning (HPKP). + + + + + + + + + + + + + + + + + + The list with pins + + + + + + + + + + + A pin is specified using the base64-encoded SPKI fingerprint as value and the + cryptographic hash algorithm as attribute + + + + + + The cryptographic hash algorithm + + + + + + + + + Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default false. + + + + + + Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + + + + + + Specifies if the browser should only report pin validation failures. Default true. + + + + + + Specifies the URI to which the browser should report pin validation failures. + + + + + + + Adds support for Content Security Policy (CSP) + + + + + + + + + + The security policy directive(s) for the Content-Security-Policy header or if report-only + is set to true, then the Content-Security-Policy-Report-Only header is used. + + + + + + Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy + violations only. Defaults to false. + + + + + + + Adds support for Referrer Policy + + + + + + + + + + The policies for the Referrer-Policy header. + + + + + + + + + + + + + + + + + + + Adds support for Feature Policy + + + + + + + + + + The security policy directive(s) for the Feature-Policy header. + + + + + + + Adds support for Permissions Policy + + + + + + + + + + The policies for the Permissions-Policy header. + + + + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for + every request + + + + + + + + + + Specifies if Cache Control should be disabled. Default false. + + + + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options + header. + + + + + + + + + + If disabled, the X-Frame-Options header will not be included. Default false. + + + + + + Specify the policy to use for the X-Frame-Options-Header. + + + + + + + + + + + + + Specify the strategy to use when ALLOW-FROM is chosen. + + + + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specify a value to use for the chosen strategy. + + + + + + Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' + based strategy. Default is 'from'. Deprecated ALLOW-FROM is an obsolete directive that no + longer works in modern browsers. Instead use Content-Security-Policy with the <a + href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors">frame-ancestors</a> + directive. + + + + + + + Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the + X-XSS-Protection header. + + + + + + + + + + disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + + + + + + specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' + meaning it is enabled. + + + + + + Add mode=block to the header or not, default is on. + + + + + + + Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + + + + + + + + + + If disabled, the X-Content-Type-Options header will not be included. Default false. + + + + + + + Adds support for Cross-Origin-Opener-Policy header + + + + + + + + + + The policies for the Cross-Origin-Opener-Policy header. + + + + + + + + + + + + + + Adds support for Cross-Origin-Embedder-Policy header + + + + + + + + + + The policies for the Cross-Origin-Embedder-Policy header. + + + + + + + + + + + + + Adds support for Cross-Origin-Resource-Policy header + + + + + + + + + + The policies for the Cross-Origin-Resource-Policy header. + + + + + + + + + + + + + + Add additional headers to the response. + + + + + + + + + + The name of the header to add. + + + + + + The value for the header. + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Used to indicate that a filter bean declaration should be incorporated into the security + filter chain. + + + + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc new file mode 100644 index 00000000000..1db6c30464a --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc @@ -0,0 +1,1333 @@ +namespace a = "https://relaxng.org/ns/compatibility/annotations/1.0" +datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" + +default namespace = "http://www.springframework.org/schema/security" + +start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider + +hash = + ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + attribute hash {"bcrypt"} +base64 = + ## Whether a string should be base64 encoded + attribute base64 {xsd:boolean} +request-matcher = + ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. + attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} +port = + ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. + attribute port { xsd:nonNegativeInteger } +url = + ## Specifies a URL. + attribute url { xsd:token } +id = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute id {xsd:token} +name = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute name {xsd:token} +ref = + ## Defines a reference to a Spring bean Id. + attribute ref {xsd:token} + +cache-ref = + ## Defines a reference to a cache for use with a UserDetailsService. + attribute cache-ref {xsd:token} + +user-service-ref = + ## A reference to a user-service (or UserDetailsService bean) Id + attribute user-service-ref {xsd:token} + +authentication-manager-ref = + ## A reference to an AuthenticationManager bean + attribute authentication-manager-ref {xsd:token} + +data-source-ref = + ## A reference to a DataSource bean + attribute data-source-ref {xsd:token} + + + +debug = + ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. + element debug {empty} + +password-encoder = + ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. + element password-encoder {password-encoder.attlist} +password-encoder.attlist &= + ref | (hash) + +role-prefix = + ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. + attribute role-prefix {xsd:token} + +use-expressions = + ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. + attribute use-expressions {xsd:boolean} + +ldap-server = + ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. + element ldap-server {ldap-server.attlist} +ldap-server.attlist &= id? +ldap-server.attlist &= (url | port)? +ldap-server.attlist &= + ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. + attribute manager-dn {xsd:string}? +ldap-server.attlist &= + ## The password for the manager DN. This is required if the manager-dn is specified. + attribute manager-password {xsd:string}? +ldap-server.attlist &= + ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff + attribute ldif { xsd:string }? +ldap-server.attlist &= + ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + attribute root { xsd:string }? +ldap-server.attlist &= + ## Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and 'unboundid'. By default, it will depends if the library is available in the classpath. + attribute mode { "apacheds" | "unboundid" }? + +ldap-server-ref-attribute = + ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + attribute server-ref {xsd:token} + + +group-search-filter-attribute = + ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. + attribute group-search-filter {xsd:token} +group-search-base-attribute = + ## Search base for group membership searches. Defaults to "" (searching from the root). + attribute group-search-base {xsd:token} +user-search-filter-attribute = + ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. + attribute user-search-filter {xsd:token} +user-search-base-attribute = + ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + attribute user-search-base {xsd:token} +group-role-attribute-attribute = + ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". + attribute group-role-attribute {xsd:token} +user-details-class-attribute = + ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + attribute user-details-class {"person" | "inetOrgPerson"} +user-context-mapper-attribute = + ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + attribute user-context-mapper-ref {xsd:token} + + +ldap-user-service = + ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + element ldap-user-service {ldap-us.attlist} +ldap-us.attlist &= id? +ldap-us.attlist &= + ldap-server-ref-attribute? +ldap-us.attlist &= + user-search-filter-attribute? +ldap-us.attlist &= + user-search-base-attribute? +ldap-us.attlist &= + group-search-filter-attribute? +ldap-us.attlist &= + group-search-base-attribute? +ldap-us.attlist &= + group-role-attribute-attribute? +ldap-us.attlist &= + cache-ref? +ldap-us.attlist &= + role-prefix? +ldap-us.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +ldap-authentication-provider = + ## Sets up an ldap authentication provider + element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} +ldap-ap.attlist &= + ldap-server-ref-attribute? +ldap-ap.attlist &= + user-search-base-attribute? +ldap-ap.attlist &= + user-search-filter-attribute? +ldap-ap.attlist &= + group-search-base-attribute? +ldap-ap.attlist &= + group-search-filter-attribute? +ldap-ap.attlist &= + group-role-attribute-attribute? +ldap-ap.attlist &= + ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. + attribute user-dn-pattern {xsd:token}? +ldap-ap.attlist &= + role-prefix? +ldap-ap.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +password-compare-element = + ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user + element password-compare {password-compare.attlist, password-encoder?} + +password-compare.attlist &= + ## The attribute in the directory which contains the user password. Defaults to "userPassword". + attribute password-attribute {xsd:token}? +password-compare.attlist &= + hash? + +intercept-methods = + ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + element intercept-methods {intercept-methods.attlist, protect+} +intercept-methods.attlist &= + ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + attribute access-decision-manager-ref {xsd:token}? + + +protect = + ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + element protect {protect.attlist, empty} +protect.attlist &= + ## A method name + attribute method {xsd:token} +protect.attlist &= + ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + attribute access {xsd:token} + +method-security-metadata-source = + ## Creates a MethodSecurityMetadataSource instance + element method-security-metadata-source {msmds.attlist, protect+} +msmds.attlist &= id? + +msmds.attlist &= use-expressions? + +method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + element method-security {method-security.attlist, expression-handler?} +method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". + attribute pre-post-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false". + attribute secured-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false". + attribute jsr250-enabled {xsd:boolean}? +method-security.attlist &= + ## If true, class-based proxying will be used instead of interface-based proxying. + attribute proxy-target-class {xsd:boolean}? + +global-method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. + element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". + attribute pre-post-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". + attribute secured-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". + attribute jsr250-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Optional AccessDecisionManager bean ID to override the default used for method security. + attribute access-decision-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor + attribute run-as-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Allows the advice "order" to be set for the method security interceptor. + attribute order {xsd:token}? +global-method-security.attlist &= + ## If true, class based proxying will be used instead of interface based proxying. + attribute proxy-target-class {xsd:boolean}? +global-method-security.attlist &= + ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. + attribute mode {"aspectj"}? +global-method-security.attlist &= + ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). + attribute metadata-source-ref {xsd:token}? +global-method-security.attlist &= + authentication-manager-ref? + + +after-invocation-provider = + ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. + element after-invocation-provider {ref} + +pre-post-annotation-handling = + ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. + element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} + +invocation-attribute-factory = + ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + element invocation-attribute-factory {ref} + +pre-invocation-advice = + ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. + element pre-invocation-advice {ref} + +post-invocation-advice = + ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. + element post-invocation-advice {ref} + + +expression-handler = + ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. + element expression-handler {ref} + +protect-pointcut = + ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. + element protect-pointcut {protect-pointcut.attlist, empty} +protect-pointcut.attlist &= + ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). + attribute expression {xsd:string} +protect-pointcut.attlist &= + ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" + attribute access {xsd:token} + +websocket-message-broker = + ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. + element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } + +websocket-message-broker.attrlist &= + ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. + attribute id {xsd:token}? +websocket-message-broker.attrlist &= + ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + attribute same-origin-disabled {xsd:boolean}? +websocket-message-broker.attrlist &= + ## Use this AuthorizationManager instead of deriving one from elements + attribute authorization-manager-ref {xsd:string}? +websocket-message-broker.attrlist &= + ## Use AuthorizationManager API instead of SecurityMetadatasource + attribute use-authorization-manager {xsd:boolean}? + +intercept-message = + ## Creates an authorization rule for a websocket message. + element intercept-message {intercept-message.attrlist} + +intercept-message.attrlist &= + ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. + attribute pattern {xsd:token}? +intercept-message.attrlist &= + ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. + attribute access {xsd:token}? +intercept-message.attrlist &= + ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? + +http-firewall = + ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. + element http-firewall {ref} + +http = + ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & saml2-login? & saml2-logout? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } +http.attlist &= + ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. + attribute pattern {xsd:token}? +http.attlist &= + ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. + attribute security {"none"}? +http.attlist &= + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref { xsd:token }? +http.attlist &= + ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. + attribute auto-config {xsd:boolean}? +http.attlist &= + use-expressions? +http.attlist &= + ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. + attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? +http.attlist &= + ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. + attribute security-context-repository-ref {xsd:token}? +http.attlist &= + ## Optional attribute that specifies that the SecurityContext should require explicit saving rather than being synchronized from the SecurityContextHolder. Defaults to "false". + attribute security-context-explicit-save {xsd:boolean}? +http.attlist &= + request-matcher? +http.attlist &= + ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". + attribute servlet-api-provision {xsd:boolean}? +http.attlist &= + ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + attribute jaas-api-provision {xsd:boolean}? +http.attlist &= + ## Use AuthorizationManager API instead of SecurityMetadataSource + attribute use-authorization-manager {xsd:boolean}? +http.attlist &= + ## Use this AuthorizationManager instead of deriving one from elements + attribute authorization-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. + attribute access-decision-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". + attribute realm {xsd:token}? +http.attlist &= + ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + attribute entry-point-ref {xsd:token}? +http.attlist &= + ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "true" + attribute once-per-request {xsd:boolean}? +http.attlist &= + ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). + attribute disable-url-rewriting {xsd:boolean}? +http.attlist &= + ## Exposes the list of filters defined by this configuration under this bean name in the application context. + name? +http.attlist &= + authentication-manager-ref? + +access-denied-handler = + ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. + element access-denied-handler {access-denied-handler.attlist, empty} +access-denied-handler.attlist &= (ref | access-denied-handler-page) + +access-denied-handler-page = + ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. + attribute error-page {xsd:token} + +intercept-url = + ## Specifies the access attributes and/or filter list for a particular set of URLs. + element intercept-url {intercept-url.attlist, empty} +intercept-url.attlist &= + (pattern | request-matcher-ref) +intercept-url.attlist &= + ## The access configuration attributes that apply for the configured path. + attribute access {xsd:token}? +intercept-url.attlist &= + ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. + attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? + +intercept-url.attlist &= + ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. + attribute requires-channel {xsd:token}? +intercept-url.attlist &= + ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. + attribute servlet-path {xsd:token}? + +logout = + ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. + element logout {logout.attlist, empty} +logout.attlist &= + ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. + attribute logout-url {xsd:token}? +logout.attlist &= + ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). + attribute logout-success-url {xsd:token}? +logout.attlist &= + ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. + attribute invalidate-session {xsd:boolean}? +logout.attlist &= + ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. + attribute success-handler-ref {xsd:token}? +logout.attlist &= + ## A comma-separated list of the names of cookies which should be deleted when the user logs out + attribute delete-cookies {xsd:token}? + +request-cache = + ## Allow the RequestCache used for saving requests during the login process to be set + element request-cache {ref} + +form-login = + ## Sets up a form login configuration for authentication with a username and password + element form-login {form-login.attlist, empty} +form-login.attlist &= + ## The URL that the login form is posted to. If unspecified, it defaults to /login. + attribute login-processing-url {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the username. Defaults to 'username'. + attribute username-parameter {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the password. Defaults to 'password'. + attribute password-parameter {xsd:token}? +form-login.attlist &= + ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. + attribute default-target-url {xsd:token}? +form-login.attlist &= + ## Whether the user should always be redirected to the default-target-url after login. + attribute always-use-default-target {xsd:boolean}? +form-login.attlist &= + ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. + attribute login-page {xsd:token}? +form-login.attlist &= + ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. + attribute authentication-failure-url {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination + attribute authentication-success-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination + attribute authentication-failure-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationFailureHandler + attribute authentication-failure-forward-url {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationSuccessHandler + attribute authentication-success-forward-url {xsd:token}? + +oauth2-login = + ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + element oauth2-login {oauth2-login.attlist} +oauth2-login.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the GrantedAuthoritiesMapper + attribute user-authorities-mapper-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2UserService + attribute user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OpenID Connect OAuth2UserService + attribute oidc-user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +oauth2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + attribute jwt-decoder-factory-ref {xsd:token}? + +oauth2-client = + ## Configures OAuth 2.0 Client support. + element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } +oauth2-client.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? + +authorization-code-grant = + ## Configures OAuth 2.0 Authorization Code Grant. + element authorization-code-grant {authorization-code-grant.attlist, empty} +authorization-code-grant.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? + +client-registrations = + ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registrations {client-registration+, provider*} + +client-registration = + ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registration {client-registration.attlist} +client-registration.attlist &= + ## The ID that uniquely identifies the client registration. + attribute registration-id {xsd:token} +client-registration.attlist &= + ## The client identifier. + attribute client-id {xsd:token} +client-registration.attlist &= + ## The client secret. + attribute client-secret {xsd:token}? +client-registration.attlist &= + ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). + attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? +client-registration.attlist &= + ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials, password and implicit. + attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password" | "implicit"}? +client-registration.attlist &= + ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. + attribute redirect-uri {xsd:token}? +client-registration.attlist &= + ## A comma-separated list of scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. + attribute scope {xsd:token}? +client-registration.attlist &= + ## A descriptive name used for the client. The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. + attribute client-name {xsd:token}? +client-registration.attlist &= + ## A reference to the associated provider. May reference a 'provider' element or use one of the common providers (google, github, facebook, okta). + attribute provider-id {xsd:token} + +provider = + ## The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + element provider {provider.attlist} +provider.attlist &= + ## The ID that uniquely identifies the provider. + attribute provider-id {xsd:token} +provider.attlist &= + ## The Authorization Endpoint URI for the Authorization Server. + attribute authorization-uri {xsd:token}? +provider.attlist &= + ## The Token Endpoint URI for the Authorization Server. + attribute token-uri {xsd:token}? +provider.attlist &= + ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. + attribute user-info-uri {xsd:token}? +provider.attlist &= + ## The authentication method used when sending the access token to the UserInfo Endpoint. The supported values are header, form and query. + attribute user-info-authentication-method {"header" | "form" | "query"}? +provider.attlist &= + ## The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + attribute user-info-user-name-attribute {xsd:token}? +provider.attlist &= + ## The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. + attribute jwk-set-uri {xsd:token}? +provider.attlist &= + ## The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + attribute issuer-uri {xsd:token}? + +oauth2-resource-server = + ## Configures authentication support as an OAuth 2.0 Resource Server. + element oauth2-resource-server {oauth2-resource-server.attlist, (jwt? & opaque-token?)} +oauth2-resource-server.attlist &= + ## Reference to an AuthenticationManagerResolver + attribute authentication-manager-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a BearerTokenResolver + attribute bearer-token-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a AuthenticationEntryPoint + attribute entry-point-ref {xsd:token}? + +jwt = + ## Configures JWT authentication + element jwt {jwt.attlist} +jwt.attlist &= + ## The URI to use to collect the JWK Set for verifying JWTs + attribute jwk-set-uri {xsd:token}? +jwt.attlist &= + ## Reference to a JwtDecoder + attribute decoder-ref {xsd:token}? +jwt.attlist &= + ## Reference to a Converter + attribute jwt-authentication-converter-ref {xsd:token}? + +opaque-token = + ## Configuration Opaque Token authentication + element opaque-token {opaque-token.attlist} +opaque-token.attlist &= + ## The URI to use to introspect opaque token attributes + attribute introspection-uri {xsd:token}? +opaque-token.attlist &= + ## The Client ID to use to authenticate the introspection request + attribute client-id {xsd:token}? +opaque-token.attlist &= + ## The Client secret to use to authenticate the introspection request + attribute client-secret {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenIntrospector + attribute introspector-ref {xsd:token}? + +openid-login = + ## Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. + element openid-login {form-login.attlist, user-service-ref?, attribute-exchange*} + +attribute-exchange = + ## Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + element attribute-exchange {attribute-exchange.attlist, openid-attribute+} + +attribute-exchange.attlist &= + ## A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + attribute identifier-match {xsd:token}? + +openid-attribute = + ## Attributes used when making an OpenID AX Fetch Request. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. + element openid-attribute {openid-attribute.attlist} + +openid-attribute.attlist &= + ## Specifies the name of the attribute that you wish to get back. For example, email. + attribute name {xsd:token} +openid-attribute.attlist &= + ## Specifies the attribute type. For example, https://axschema.org/contact/email. See your OP's documentation for valid attribute types. + attribute type {xsd:token} +openid-attribute.attlist &= + ## Specifies if this attribute is required to the OP, but does not error out if the OP does not return the attribute. Default is false. + attribute required {xsd:boolean}? +openid-attribute.attlist &= + ## Specifies the number of attributes that you wish to get back. For example, return 3 emails. The default value is 1. + attribute count {xsd:int}? + +saml2-login = + ## Configures authentication support for SAML 2.0 Login + element saml2-login {saml2-login.attlist} +saml2-login.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestRepository + attribute authentication-request-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestResolver + attribute authentication-request-resolver-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationConverter + attribute authentication-converter-ref {xsd:token}? +saml2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +saml2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationManager + attribute authentication-manager-ref {xsd:token}? + +saml2-logout = + ## Configures SAML 2.0 Single Logout support + element saml2-logout {saml2-logout.attlist} +saml2-logout.attlist &= + ## The URL by which the relying or asserting party can trigger logout + attribute logout-url {xsd:token}? +saml2-logout.attlist &= + ## The URL by which the asserting party can send a SAML 2.0 Logout Request + attribute logout-request-url {xsd:token}? +saml2-logout.attlist &= + ## The URL by which the asserting party can send a SAML 2.0 Logout Response + attribute logout-response-url {xsd:token}? +saml2-logout.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestValidator + attribute logout-request-validator-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestResolver + attribute logout-request-resolver-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestRepository + attribute logout-request-repository-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutResponseValidator + attribute logout-response-validator-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutResponseResolver + attribute logout-response-resolver-ref {xsd:token}? + +relying-party-registrations = + ## Container element for relying party(ies) registered with a SAML 2.0 identity provider + element relying-party-registrations {relying-party-registration+, asserting-party*} + +relying-party-registration = + ## Represents a relying party registered with a SAML 2.0 identity provider + element relying-party-registration {relying-party-registration.attlist, signing-credential*, decryption-credential*} +relying-party-registration.attlist &= + ## The ID that uniquely identifies the relying party registration. + attribute registration-id {xsd:token} +relying-party-registration.attlist &= + ## The location of the Identity Provider's metadata. + attribute metadata-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party's EntityID + attribute entity-id {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Location + attribute assertion-consumer-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Binding + attribute assertion-consumer-service-binding {xsd:token}? +relying-party-registration.attlist &= + ## A reference to the associated asserting party. + attribute asserting-party-id {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? + +signing-credential = + ## The relying party's signing credential + element signing-credential {signing-credential.attlist} +signing-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +signing-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +decryption-credential = + ## The relying party's decryption credential + element decryption-credential {decryption-credential.attlist} +decryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +decryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +asserting-party = + ## The configuration metadata of the Asserting party + element asserting-party {asserting-party.attlist, verification-credential*, encryption-credential*} +asserting-party.attlist &= + ## A unique identifier of the asserting party. + attribute asserting-party-id {xsd:token} +asserting-party.attlist &= + ## The asserting party's EntityID. + attribute entity-id {xsd:token} +asserting-party.attlist &= + ## Indicates the asserting party's preference that relying parties should sign the AuthnRequest before sending + attribute want-authn-requests-signed {xsd:token}? +asserting-party.attlist &= + ## The SingleSignOnService Location. + attribute single-sign-on-service-location {xsd:token} +asserting-party.attlist &= + ## The SingleSignOnService Binding. + attribute single-sign-on-service-binding {xsd:token}? +asserting-party.attlist &= + ## A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this asserting party, in preference order. + attribute signing-algorithms {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? + +verification-credential = + ## The relying party's verification credential + element verification-credential {verification-credential.attlist} +verification-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +verification-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +encryption-credential = + ## The asserting party's encryption credential + element encryption-credential {encryption-credential.attlist} +encryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +encryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +filter-chain-map = + ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + element filter-chain-map {filter-chain-map.attlist, filter-chain+} +filter-chain-map.attlist &= + request-matcher? + +filter-chain = + ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. + element filter-chain {filter-chain.attlist, empty} +filter-chain.attlist &= + (pattern | request-matcher-ref) +filter-chain.attlist &= + ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + attribute filters {xsd:token} + +pattern = + ## The request URL pattern which will be mapped to the FilterChain. + attribute pattern {xsd:token} +request-matcher-ref = + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref {xsd:token} + +filter-security-metadata-source = + ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. + element filter-security-metadata-source {fsmds.attlist, intercept-url+} +fsmds.attlist &= + use-expressions? +fsmds.attlist &= + id? +fsmds.attlist &= + request-matcher? + +http-basic = + ## Adds support for basic authentication + element http-basic {http-basic.attlist, empty} + +http-basic.attlist &= + ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + attribute entry-point-ref {xsd:token}? +http-basic.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +password-management = + ## Adds support for the password management. + element password-management {password-management.attlist, empty} + +password-management.attlist &= + ## The change password page. Defaults to "/change-password". + attribute change-password-page {xsd:string}? + +session-management = + ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. + element session-management {session-management.attlist, concurrency-control?} + +session-management.attlist &= + ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. + attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? +session-management.attlist &= + ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. + attribute invalid-session-url {xsd:token}? +session-management.attlist &= + ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter + attribute invalid-session-strategy-ref {xsd:token}? +session-management.attlist &= + ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter + attribute session-authentication-strategy-ref {xsd:token}? +session-management.attlist &= + ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. + attribute session-authentication-error-url {xsd:token}? + + +concurrency-control = + ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. + element concurrency-control {concurrency-control.attlist, empty} + +concurrency-control.attlist &= + ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. + attribute max-sessions {xsd:token}? +concurrency-control.attlist &= + ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. + attribute expired-url {xsd:token}? +concurrency-control.attlist &= + ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter + attribute expired-session-strategy-ref {xsd:token}? +concurrency-control.attlist &= + ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. + attribute error-if-maximum-exceeded {xsd:boolean}? +concurrency-control.attlist &= + ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. + attribute session-registry-alias {xsd:token}? +concurrency-control.attlist &= + ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. + attribute session-registry-ref {xsd:token}? + + +remember-me = + ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. + element remember-me {remember-me.attlist} +remember-me.attlist &= + ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? + +remember-me.attlist &= + (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) + +remember-me.attlist &= + user-service-ref? + +remember-me.attlist &= + ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. + attribute services-alias {xsd:token}? + +remember-me.attlist &= + ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. + attribute use-secure-cookie {xsd:boolean}? + +remember-me.attlist &= + ## The period (in seconds) for which the remember-me cookie should be valid. + attribute token-validity-seconds {xsd:string}? + +remember-me.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. + attribute authentication-success-handler-ref {xsd:token}? +remember-me.attlist &= + ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-parameter {xsd:token}? +remember-me.attlist &= + ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-cookie {xsd:token}? + +token-repository-ref = + ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. + attribute token-repository-ref {xsd:token} +remember-me-services-ref = + ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. + attribute services-ref {xsd:token}? +remember-me-data-source-ref = + ## DataSource bean for the database that contains the token repository schema. + data-source-ref + +anonymous = + ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. + element anonymous {anonymous.attlist} +anonymous.attlist &= + ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? +anonymous.attlist &= + ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". + attribute username {xsd:token}? +anonymous.attlist &= + ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + attribute granted-authority {xsd:token}? +anonymous.attlist &= + ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. + attribute enabled {xsd:boolean}? + + +port-mappings = + ## Defines the list of mappings between http and https ports for use in redirects + element port-mappings {port-mappings.attlist, port-mapping+} + +port-mappings.attlist &= empty + +port-mapping = + ## Provides a method to map http ports to https ports when forcing a redirect. + element port-mapping {http-port, https-port} + +http-port = + ## The http port to use. + attribute http {xsd:token} + +https-port = + ## The https port to use. + attribute https {xsd:token} + + +x509 = + ## Adds support for X.509 client authentication. + element x509 {x509.attlist} +x509.attlist &= + ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". + attribute subject-principal-regex {xsd:token}? +x509.attlist &= + ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. + user-service-ref? +x509.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +jee = + ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. + element jee {jee.attlist} +jee.attlist &= + ## A comma-separate list of roles to look for in the incoming HttpServletRequest. + attribute mappable-roles {xsd:token} +jee.attlist &= + ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. + user-service-ref? + +authentication-manager = + ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. + element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} +authman.attlist &= + id? +authman.attlist &= + ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) + attribute alias {xsd:token}? +authman.attlist &= + ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. + attribute erase-credentials {xsd:boolean}? + +authentication-provider = + ## Indicates that the contained user-service should be used as an authentication source. + element authentication-provider {ap.attlist & any-user-service & password-encoder?} +ap.attlist &= + ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. + ref? +ap.attlist &= + ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. + user-service-ref? + +user-service = + ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + element user-service {id? & (properties-file | (user*))} +properties-file = + ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + attribute properties {xsd:token}? + +user = + ## Represents a user in the application. + element user {user.attlist, empty} +user.attlist &= + ## The username assigned to the user. + attribute name {xsd:token} +user.attlist &= + ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. + attribute password {xsd:string}? +user.attlist &= + ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + attribute authorities {xsd:token} +user.attlist &= + ## Can be set to "true" to mark an account as locked and unusable. + attribute locked {xsd:boolean}? +user.attlist &= + ## Can be set to "true" to mark an account as disabled and unusable. + attribute disabled {xsd:boolean}? + +jdbc-user-service = + ## Causes creation of a JDBC-based UserDetailsService. + element jdbc-user-service {id? & jdbc-user-service.attlist} +jdbc-user-service.attlist &= + ## The bean ID of the DataSource which provides the required tables. + attribute data-source-ref {xsd:token} +jdbc-user-service.attlist &= + cache-ref? +jdbc-user-service.attlist &= + ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" + attribute users-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" + attribute authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + attribute group-authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + role-prefix? + +csrf = +## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. + element csrf {csrf-options.attlist} +csrf-options.attlist &= + ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). + attribute disabled {xsd:boolean}? +csrf-options.attlist &= + ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + attribute request-matcher-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository. + attribute token-repository-ref { xsd:token }? + +headers = +## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & cross-origin-opener-policy? & cross-origin-embedder-policy? & cross-origin-resource-policy? & header*)} +headers-options.attlist &= + ## Specifies if the default headers should be disabled. Default false. + attribute defaults-disabled {xsd:token}? +headers-options.attlist &= + ## Specifies if headers should be disabled. Default false. + attribute disabled {xsd:token}? +hsts = + ## Adds support for HTTP Strict Transport Security (HSTS) + element hsts {hsts-options.attlist} +hsts-options.attlist &= + ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hsts-options.attlist &= + ## Specifies if subdomains should be included. Default true. + attribute include-subdomains {xsd:boolean}? +hsts-options.attlist &= + ## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year. + attribute max-age-seconds {xsd:integer}? +hsts-options.attlist &= + ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. + attribute request-matcher-ref { xsd:token }? +hsts-options.attlist &= + ## Specifies if preload should be included. Default false. + attribute preload {xsd:boolean}? + +cors = +## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource +element cors { cors-options.attlist } +cors-options.attlist &= + ref? +cors-options.attlist &= + ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use + attribute configuration-source-ref {xsd:token}? + +hpkp = + ## Adds support for HTTP Public Key Pinning (HPKP). + element hpkp {hpkp.pins,hpkp.attlist} +hpkp.pins = + ## The list with pins + element pins {hpkp.pin+} +hpkp.pin = + ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute + element pin { + ## The cryptographic hash algorithm + attribute algorithm { xsd:string }?, + text + } +hpkp.attlist &= + ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hpkp.attlist &= + ## Specifies if subdomains should be included. Default false. + attribute include-subdomains {xsd:boolean}? +hpkp.attlist &= + ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + attribute max-age-seconds {xsd:integer}? +hpkp.attlist &= + ## Specifies if the browser should only report pin validation failures. Default true. + attribute report-only {xsd:boolean}? +hpkp.attlist &= + ## Specifies the URI to which the browser should report pin validation failures. + attribute report-uri {xsd:string}? + +content-security-policy = + ## Adds support for Content Security Policy (CSP) + element content-security-policy {csp-options.attlist} +csp-options.attlist &= + ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + attribute policy-directives {xsd:token}? +csp-options.attlist &= + ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + attribute report-only {xsd:boolean}? + +referrer-policy = + ## Adds support for Referrer Policy + element referrer-policy {referrer-options.attlist} +referrer-options.attlist &= + ## The policies for the Referrer-Policy header. + attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? + +feature-policy = + ## Adds support for Feature Policy + element feature-policy {feature-options.attlist} +feature-options.attlist &= + ## The security policy directive(s) for the Feature-Policy header. + attribute policy-directives {xsd:token}? + +permissions-policy = + ## Adds support for Permissions Policy + element permissions-policy {permissions-options.attlist} +permissions-options.attlist &= + ## The policies for the Permissions-Policy header. + attribute policy {xsd:token}? + +cache-control = + ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request + element cache-control {cache-control.attlist} +cache-control.attlist &= + ## Specifies if Cache Control should be disabled. Default false. + attribute disabled {xsd:boolean}? + +frame-options = + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} +frame-options.attlist &= + ## If disabled, the X-Frame-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? +frame-options.attlist &= + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? +frame-options.attlist &= + ## Specify the strategy to use when ALLOW-FROM is chosen. + attribute strategy {"static","whitelist","regexp"}? +frame-options.attlist &= + ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. + ref? +frame-options.attlist &= + ## Specify a value to use for the chosen strategy. + attribute value {xsd:string}? +frame-options.attlist &= + ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. + ## Deprecated ALLOW-FROM is an obsolete directive that no longer works in modern browsers. Instead use + ## Content-Security-Policy with the + ## frame-ancestors + ## directive. + attribute from-parameter {xsd:string}? + + +xss-protection = + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} +xss-protection.attlist &= + ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + attribute disabled {xsd:boolean}? +xss-protection.attlist &= + ## specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? +xss-protection.attlist &= + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? + +content-type-options = + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {content-type-options.attlist, empty} +content-type-options.attlist &= + ## If disabled, the X-Content-Type-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? + +cross-origin-opener-policy = + ## Adds support for Cross-Origin-Opener-Policy header + element cross-origin-opener-policy {cross-origin-opener-policy-options.attlist,empty} +cross-origin-opener-policy-options.attlist &= + ## The policies for the Cross-Origin-Opener-Policy header. + attribute policy {"unsafe-none","same-origin","same-origin-allow-popups"}? + +cross-origin-embedder-policy = + ## Adds support for Cross-Origin-Embedder-Policy header + element cross-origin-embedder-policy {cross-origin-embedder-policy-options.attlist,empty} +cross-origin-embedder-policy-options.attlist &= + ## The policies for the Cross-Origin-Embedder-Policy header. + attribute policy {"unsafe-none","require-corp"}? + +cross-origin-resource-policy = + ## Adds support for Cross-Origin-Resource-Policy header + element cross-origin-resource-policy {cross-origin-resource-policy-options.attlist,empty} +cross-origin-resource-policy-options.attlist &= + ## The policies for the Cross-Origin-Resource-Policy header. + attribute policy {"cross-origin","same-origin","same-site"}? + +header= + ## Add additional headers to the response. + element header {header.attlist} +header.attlist &= + ## The name of the header to add. + attribute name {xsd:token}? +header.attlist &= + ## The value for the header. + attribute value {xsd:token}? +header.attlist &= + ## Reference to a custom HeaderWriter implementation. + ref? + +any-user-service = user-service | jdbc-user-service | ldap-user-service + +custom-filter = + ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. + element custom-filter {custom-filter.attlist} + +custom-filter.attlist &= + ref + +custom-filter.attlist &= + (after | before | position) + +after = + ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. + attribute after {named-security-filter} +before = + ## The filter immediately before which the custom-filter should be placed in the chain + attribute before {named-security-filter} +position = + ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. + attribute position {named-security-filter} + +named-security-filter = "FIRST" | "DISABLE_ENCODE_URL_FILTER" | "FORCE_EAGER_SESSION_FILTER" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "SAML2_LOGOUT_REQUEST_FILTER" | "SAML2_LOGOUT_RESPONSE_FILTER" | "CSRF_FILTER" | "SAML2_LOGOUT_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd new file mode 100644 index 00000000000..2d3538a09d5 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd @@ -0,0 +1,3790 @@ + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + Whether a string should be base64 encoded + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + + + Specifies a URL. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + A reference to an AuthenticationManager bean + + + + + + + + A reference to a DataSource bean + + + + + + + Enables Spring Security debugging infrastructure. This will provide human-readable + (multi-line) debugging information to monitor requests coming into the security filters. + This may include sensitive information, such as request parameters or headers, and should + only be used in a development environment. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Defines an LDAP server location or starts an embedded server. The url indicates the + location of a remote server. If no url is given, an embedded server will be started, + listening on the supplied port number. The port is optional and defaults to 33389. A + Spring LDAP ContextSource bean will be registered for the server with the id supplied. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Specifies a URL. + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + Username (DN) of the "manager" user identity which will be used to authenticate to a + (non-embedded) LDAP server. If omitted, anonymous access will be used. + + + + + + The password for the manager DN. This is required if the manager-dn is specified. + + + + + + Explicitly specifies an ldif file resource to load into an embedded LDAP server. The + default is classpath*:*.ldiff + + + + + + Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + + + + + + Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and + 'unboundid'. By default, it will depends if the library is available in the classpath. + + + + + + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + This element configures a LdapUserDetailsService which is a combination of a + FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key + "{0}" must be present and will be substituted with the username. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The attribute in the directory which contains the user password. Defaults to + "userPassword". + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + Can be used inside a bean definition to add a security interceptor to the bean and set up + access configuration attributes for the bean's methods + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + Optional AccessDecisionManager bean ID to be used by the created method security + interceptor. + + + + + + + + + A method name + + + + + + Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + + + + + + + Creates a MethodSecurityMetadataSource instance + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with Spring Security annotations. Where + there is a match, the beans will automatically be proxied and security authorization + applied to the methods accordingly. Interceptors are invoked in the order specified in + AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "true". + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "false". + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "false". + + + + + + If true, class-based proxying will be used instead of interface-based proxying. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with the ordered list of + "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a + match, the beans will automatically be proxied and security authorization applied to the + methods accordingly. If you use and enable all four sources of method security metadata + (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 + security annotations), the metadata sources will be queried in that order. In practical + terms, this enables you to use XML to override method security metadata expressed in + annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize + etc.), @Secured and finally JSR-250. + + + + + + + + Allows the default expression-based mechanism for handling Spring Security's pre and post + invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be + replace entirely. Only applies if these annotations are enabled. + + + + + + + Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and + post invocation metadata from the annotated methods. + + + + + + + + + Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the + PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. + + + + + + + + + Customizes the PostInvocationAdviceProvider with the ref as the + PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + + + + Allows addition of extra AfterInvocationProvider beans which should be called by the + MethodSecurityInterceptor created by global-method-security. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "disabled". + + + + + + + + + + + + Optional AccessDecisionManager bean ID to override the default used for method security. + + + + + + Optional RunAsmanager implementation which will be used by the configured + MethodSecurityInterceptor + + + + + + Allows the advice "order" to be set for the method security interceptor. + + + + + + If true, class based proxying will be used instead of interface based proxying. + + + + + + Can be used to specify that AspectJ should be used instead of the default Spring AOP. If + set, secured classes must be woven with the AnnotationSecurityAspect from the + spring-security-aspects module. + + + + + + + + + + + An external MethodSecurityMetadataSource instance can be supplied which will take priority + over other sources (such as the default annotations). + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + + + + + + + An AspectJ expression, including the 'execution' keyword. For example, 'execution(int + com.foo.TargetObject.countLength(String))' (without the quotes). + + + + + + Access configuration attributes list that applies to all methods matching the pointcut, + e.g. "ROLE_A,ROLE_B" + + + + + + + Allows securing a Message Broker. There are two modes. If no id is specified: ensures that + any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver + registered as a custom argument resolver; ensures that the + SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that + can be manually registered with the clientInboundChannel. + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. If specified, + explicit configuration within clientInboundChannel is required. If not specified, ensures + that any SimpAnnotationMethodMessageHandler has the + AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures + that the SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. + + + + + + Disables the requirement for CSRF token to be present in the Stomp headers (default + false). Changing the default is useful if it is necessary to allow other origins to make + SockJS connections. + + + + + + Use this AuthorizationManager instead of deriving one from <intercept-message> elements + + + + + + Use AuthorizationManager API instead of SecurityMetadatasource + + + + + + + Creates an authorization rule for a websocket message. + + + + + + + + + + The destination ant pattern which will be mapped to the access attribute. For example, /** + matches any message with a destination, /admin/** matches any message that has a + destination that starts with admin. + + + + + + The access configuration attributes that apply for the configured message. For example, + permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role + 'ROLE_ADMIN'. + + + + + + The type of message to match on. Valid values are defined in SimpMessageType (i.e. + CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, + DISCONNECT_ACK, OTHER). + + + + + + + + + + + + + + + + + + + + Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created + by the namespace. + + + + + + + + + Container element for HTTP security configuration. Multiple elements can now be defined, + each with a specific pattern to which the enclosed security configuration applies. A + pattern can also be configured to bypass Spring Security's filters completely by setting + the "security" attribute to "none". + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + Defines the access-denied strategy that should be used. An access denied page can be + defined or a reference to an AccessDeniedHandler instance. + + + + + + + + + Sets up a form login configuration for authentication with a username and password + + + + + + + + + + + + Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and + 2.0 protocols have been deprecated and users are <a + href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to + migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is + supported by <code>spring-security-oauth2</code>. + + + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + Configures authentication support for SAML 2.0 Login + + + + + + + + + Configures SAML 2.0 Single Logout support + + + + + + + + + Adds support for X.509 client authentication. + + + + + + + + + + Adds support for basic authentication + + + + + + + + + Incorporates a logout processing filter. Most web applications require a logout filter, + although you may not require one if you write a controller to provider similar logic. + + + + + + + + + + Session-management related functionality is implemented by the addition of a + SessionManagementFilter to the filter stack. + + + + + + + Enables concurrent session control, limiting the number of authenticated sessions a user + may have at the same time. + + + + + + + + + + + + + Sets up remember-me authentication. If used with the "key" attribute (or no attributes) + the cookie-only implementation will be used. Specifying "token-repository-ref" or + "remember-me-data-source-ref" will use the more secure, persisten token approach. + + + + + + + + + Adds support for automatically granting all anonymous web requests a particular principal + identity and a corresponding granted authority. + + + + + + + + + Defines the list of mappings between http and https ports for use in redirects + + + + + + + Provides a method to map http ports to https ports when forcing a redirect. + + + + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + + + + The request URL pattern which will be mapped to the filter chain created by this <http> + element. If omitted, the filter chain will match all requests. + + + + + + When set to 'none', requests matching the pattern attribute will be ignored by Spring + Security. No security filters will be applied and no SecurityContext will be available. If + set, the <http> element must be empty, with no children. + + + + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A legacy attribute which automatically registers a login form, BASIC authentication and a + logout URL and logout services. If unspecified, defaults to "false". We'd recommend you + avoid using this and instead explicitly configure the services you require. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + Controls the eagerness with which an HTTP session is created by Spring Security classes. + If not set, defaults to "ifRequired". If "stateless" is used, this implies that the + application guarantees that it will not create a session. This differs from the use of + "never" which means that Spring Security will not create a session, but will make use of + one if the application does. + + + + + + + + + + + + + + A reference to a SecurityContextRepository bean. This can be used to customize how the + SecurityContext is stored between requests. + + + + + + Optional attribute that specifies that the SecurityContext should require explicit saving + rather than being synchronized from the SecurityContextHolder. Defaults to "false". + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + Provides versions of HttpServletRequest security methods such as isUserInRole() and + getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to + "true". + + + + + + If available, runs the request as the Subject acquired from the JaasAuthenticationToken. + Defaults to "false". + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the realm name that will be used for all authentication + features that require a realm name (eg BASIC and Digest authentication). If unspecified, + defaults to "Spring Security Application". + + + + + + Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + + + + + + Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults + to "true" + + + + + + Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" + (rewriting is disabled). + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + The access configuration attributes that apply for the configured path. + + + + + + The HTTP Method for which the access configuration attributes should apply. If not + specified, the attributes will apply to any method. + + + + + + + + + + + + + + + + + + Used to specify that a URL must be accessed over http or https, or that there is no + preference. The value should be "http", "https" or "any", respectively. + + + + + + The path to the servlet. This attribute is only applicable when 'request-matcher' is + 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are + 2 or more HttpServlet's registered in the ServletContext that have mappings starting with + '/' and are different; 2) The pattern starts with the same value of a registered + HttpServlet path, excluding the default (root) HttpServlet '/'. + + + + + + + + + Specifies the URL that will cause a logout. Spring Security will initialize a filter that + responds to this particular URL. Defaults to /logout if unspecified. + + + + + + Specifies the URL to display once the user has logged out. If not specified, defaults to + <form-login-login-page>/?logout (i.e. /login?logout). + + + + + + Specifies whether a logout also causes HttpSession invalidation, which is generally + desirable. If unspecified, defaults to true. + + + + + + A reference to a LogoutSuccessHandler implementation which will be used to determine the + destination to which the user is taken after logging out. + + + + + + A comma-separated list of the names of cookies which should be deleted when the user logs + out + + + + + + + Allow the RequestCache used for saving requests during the login process to be set + + + + + + + + + + + The URL that the login form is posted to. If unspecified, it defaults to /login. + + + + + + The name of the request parameter which contains the username. Defaults to 'username'. + + + + + + The name of the request parameter which contains the password. Defaults to 'password'. + + + + + + The URL that will be redirected to after successful authentication, if the user's previous + action could not be resumed. This generally happens if the user visits a login page + without having first requested a secured operation that triggers authentication. If + unspecified, defaults to the root of the application. + + + + + + Whether the user should always be redirected to the default-target-url after login. + + + + + + The URL for the login page. If no login URL is specified, Spring Security will + automatically create a login URL at GET /login and a corresponding filter to render that + login URL when requested. + + + + + + The URL for the login failure page. If no login failure URL is specified, Spring Security + will automatically create a failure login URL at /login?error and a corresponding filter + to render that login failure URL when requested. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful authentication request. Should not be used in combination with + default-target-url (or always-use-default-target-url) as the implementation should always + deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationFailureHandler bean which should be used to handle a failed + authentication request. Should not be used in combination with authentication-failure-url + as the implementation should always deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + The URL for the ForwardAuthenticationFailureHandler + + + + + + The URL for the ForwardAuthenticationSuccessHandler + + + + + + + Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + Reference to the GrantedAuthoritiesMapper + + + + + + Reference to the OAuth2UserService + + + + + + Reference to the OpenID Connect OAuth2UserService + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + + + + + + + Configures OAuth 2.0 Client support. + + + + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + + Configures OAuth 2.0 Authorization Code Grant. + + + + + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + + Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 + Provider. + + + + + + + + + + + + Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the client registration. + + + + + + The client identifier. + + + + + + The client secret. + + + + + + The method used to authenticate the client with the provider. The supported values are + client_secret_basic, client_secret_post and none (public clients). + + + + + + + + + + + + + + + The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The + supported values are authorization_code, client_credentials, password and implicit. + + + + + + + + + + + + + + The client’s registered redirect URI that the Authorization Server redirects the + end-user’s user-agent to after the end-user has authenticated and authorized access to the + client. + + + + + + A comma-separated list of scope(s) requested by the client during the Authorization + Request flow, such as openid, email, or profile. + + + + + + A descriptive name used for the client. The name may be used in certain scenarios, such as + when displaying the name of the client in the auto-generated login page. + + + + + + A reference to the associated provider. May reference a 'provider' element or use one of + the common providers (google, github, facebook, okta). + + + + + + + The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the provider. + + + + + + The Authorization Endpoint URI for the Authorization Server. + + + + + + The Token Endpoint URI for the Authorization Server. + + + + + + The UserInfo Endpoint URI used to access the claims/attributes of the authenticated + end-user. + + + + + + The authentication method used when sending the access token to the UserInfo Endpoint. The + supported values are header, form and query. + + + + + + + + + + + + + The name of the attribute returned in the UserInfo Response that references the Name or + Identifier of the end-user. + + + + + + The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which + contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID + Token and optionally the UserInfo Response. + + + + + + The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect + 1.0 Provider. + + + + + + + Configures authentication support as an OAuth 2.0 Resource Server. + + + + + + + + + + + + + + Reference to an AuthenticationManagerResolver + + + + + + Reference to a BearerTokenResolver + + + + + + Reference to a AuthenticationEntryPoint + + + + + + + Configures JWT authentication + + + + + + + + + + The URI to use to collect the JWK Set for verifying JWTs + + + + + + Reference to a JwtDecoder + + + + + + Reference to a Converter<Jwt, AbstractAuthenticationToken> + + + + + + + Configuration Opaque Token authentication + + + + + + + + + + The URI to use to introspect opaque token attributes + + + + + + The Client ID to use to authenticate the introspection request + + + + + + The Client secret to use to authenticate the introspection request + + + + + + Reference to an OpaqueTokenIntrospector + + + + + + + + Sets up an attribute exchange configuration to request specified attributes from the + OpenID identity provider. When multiple elements are used, each must have an + identifier-attribute attribute. Each configuration will be matched in turn against the + supplied login identifier until a match is found. + + + + + + + + + + + + + A regular expression which will be compared against the claimed identity, when deciding + which attribute-exchange configuration to use during authentication. + + + + + + + Attributes used when making an OpenID AX Fetch Request. NOTE: The OpenID 1.0 and 2.0 + protocols have been deprecated and users are <a + href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to + migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is + supported by <code>spring-security-oauth2</code>. + + + + + + + + + + Specifies the name of the attribute that you wish to get back. For example, email. + + + + + + Specifies the attribute type. For example, https://axschema.org/contact/email. See your + OP's documentation for valid attribute types. + + + + + + Specifies if this attribute is required to the OP, but does not error out if the OP does + not return the attribute. Default is false. + + + + + + Specifies the number of attributes that you wish to get back. For example, return 3 + emails. The default value is 1. + + + + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2AuthenticationRequestRepository + + + + + + Reference to the Saml2AuthenticationRequestResolver + + + + + + Reference to the AuthenticationConverter + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the AuthenticationManager + + + + + + + + + The URL by which the relying or asserting party can trigger logout + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Request + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Response + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2LogoutRequestValidator + + + + + + Reference to the Saml2LogoutRequestResolver + + + + + + Reference to the Saml2LogoutRequestRepository + + + + + + Reference to the Saml2LogoutResponseValidator + + + + + + Reference to the Saml2LogoutResponseResolver + + + + + + + Container element for relying party(ies) registered with a SAML 2.0 identity provider + + + + + + + + + + + + Represents a relying party registered with a SAML 2.0 identity provider + + + + + + + + + + + + + + The ID that uniquely identifies the relying party registration. + + + + + + The location of the Identity Provider's metadata. + + + + + + The relying party's EntityID + + + + + + The Assertion Consumer Service Location + + + + + + The Assertion Consumer Service Binding + + + + + + A reference to the associated asserting party. + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Location</a> + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Response Location</a> + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Binding</a> + + + + + + + The relying party's signing credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The relying party's decryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The configuration metadata of the Asserting party + + + + + + + + + + + + + + A unique identifier of the asserting party. + + + + + + The asserting party's EntityID. + + + + + + Indicates the asserting party's preference that relying parties should sign the + AuthnRequest before sending + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Location. + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Binding. + + + + + + A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this + asserting party, in preference order. + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Location</a> + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Response Location</a> + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Binding</a> + + + + + + + The relying party's verification credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The asserting party's encryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + + + + + + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + Used within to define a specific URL pattern and the list of filters which apply to the + URLs matching that pattern. When multiple filter-chain elements are assembled in a list in + order to configure a FilterChainProxy, the most specific patterns must be placed at the + top of the list, with most general ones at the bottom. + + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A comma separated list of bean names that implement Filter that should be processed for + this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + + Used to explicitly configure a FilterSecurityMetadataSource bean for use with a + FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy + explicitly, rather than using the <http> element. The intercept-url elements used should + only contain pattern, method and access attributes. Any others will result in a + configuration error. + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + + Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds support for the password management. + + + + + + + + + + The change password page. Defaults to "/change-password". + + + + + + + + + Indicates how session fixation protection will be applied when a user authenticates. If + set to "none", no protection will be applied. "newSession" will create a new empty + session, with only Spring Security-related attributes migrated. "migrateSession" will + create a new session and copy all session attributes to the new session. In Servlet 3.1 + (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing + session and use the container-supplied session fixation protection + (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and + newer containers, "migrateSession" in older containers. Throws an exception if + "changeSessionId" is used in older containers. + + + + + + + + + + + + + + The URL to which a user will be redirected if they submit an invalid session indentifier. + Typically used to detect session timeouts. + + + + + + Allows injection of the InvalidSessionStrategy instance used by the + SessionManagementFilter + + + + + + Allows injection of the SessionAuthenticationStrategy instance used by the + SessionManagementFilter + + + + + + Defines the URL of the error page which should be shown when the + SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error + code will be returned to the client. Note that this attribute doesn't apply if the error + occurs during a form-based login, where the URL for authentication failure will take + precedence. + + + + + + + + + The maximum number of sessions a single authenticated user can have open at the same time. + Defaults to "1". A negative value denotes unlimited sessions. + + + + + + The URL a user will be redirected to if they attempt to use a session which has been + "expired" because they have logged in again. + + + + + + Allows injection of the SessionInformationExpiredStrategy instance used by the + ConcurrentSessionFilter + + + + + + Specifies that an unauthorized error should be reported when a user attempts to login when + they already have the maximum configured sessions open. The default behaviour is to expire + the original session. If the session-authentication-error-url attribute is set on the + session-management URL, the user will be redirected to this URL. + + + + + + Allows you to define an alias for the SessionRegistry bean in order to access it in your + own configuration. + + + + + + Allows you to define an external SessionRegistry bean to be used by the concurrency + control setup. + + + + + + + + + The "key" used to identify cookies from a specific token-based remember-me application. + You should set this to a unique value for your application. If unset, it will default to a + random value generated by SecureRandom. + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + A reference to a DataSource bean + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Exports the internally defined RememberMeServices as a bean alias, allowing it to be used + by other beans in the application context. + + + + + + Determines whether the "secure" flag will be set on the remember-me cookie. If set to + true, the cookie will only be submitted over HTTPS (recommended). By default, secure + cookies will be used if the request is made on a secure connection. + + + + + + The period (in seconds) for which the remember-me cookie should be valid. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful remember-me authentication. + + + + + + The name of the request parameter which toggles remember-me authentication. Defaults to + 'remember-me'. + + + + + + The name of cookie which store the token for remember-me authentication. Defaults to + 'remember-me'. + + + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + + + Allows a custom implementation of RememberMeServices to be used. Note that this + implementation should return RememberMeAuthenticationToken instances with the same "key" + value as specified in the remember-me element. Alternatively it should register its own + AuthenticationProvider. It should also implement the LogoutHandler interface, which will + be invoked when a user logs out. Typically the remember-me cookie would be removed on + logout. + + + + + + + + + + + + The key shared between the provider and filter. This generally does not need to be set. If + unset, it will default to a random value generated by SecureRandom. + + + + + + The username that should be assigned to the anonymous request. This allows the principal + to be identified, which may be important for logging and auditing. if unset, defaults to + "anonymousUser". + + + + + + The granted authority that should be assigned to the anonymous request. Commonly this is + used to assign the anonymous request particular roles, which can subsequently be used in + authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + + + + + + With the default namespace setup, the anonymous "authentication" facility is automatically + enabled. You can disable it using this property. + + + + + + + + + + The http port to use. + + + + + + + + The https port to use. + + + + + + + + + The regular expression used to obtain the username from the certificate's subject. + Defaults to matching on the common name using the pattern "CN=(.*?),". + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration + with container authentication. + + + + + + + + + + A comma-separate list of roles to look for in the incoming HttpServletRequest. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Registers the AuthenticationManager instance and allows its list of + AuthenticationProviders to be defined. Also allows you to define an alias to allow you to + reference the AuthenticationManager in your own beans. + + + + + + + Indicates that the contained user-service should be used as an authentication source. + + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + Sets up an ldap authentication provider + + + + + + + Specifies that an LDAP provider should use an LDAP compare operation of the user's + password to authenticate the user + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + An alias you wish to use for the AuthenticationManager bean (not required it you are using + a specific id) + + + + + + If set to true, the AuthenticationManger will attempt to clear any credentials data in the + returned Authentication object, once the user has been authenticated. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Creates an in-memory UserDetailsService from a properties file or a list of "user" child + elements. Usernames are converted to lower-case internally to allow for case-insensitive + lookups, so this should not be used if case-sensitivity is required. + + + + + + + Represents a user in the application. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The location of a Properties file where each line is in the format of + username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + + + + + + + + + The username assigned to the user. + + + + + + The password assigned to the user. This may be hashed if the corresponding authentication + provider supports hashing (remember to set the "hash" attribute of the "user-service" + element). This attribute be omitted in the case where the data will not be used for + authentication, but only for accessing authorities. If omitted, the namespace will + generate a random value, preventing its accidental use for authentication. Cannot be + empty. + + + + + + One of more authorities granted to the user. Separate authorities with a comma (but no + space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + + + + + Can be set to "true" to mark an account as locked and unusable. + + + + + + Can be set to "true" to mark an account as disabled and unusable. + + + + + + + Causes creation of a JDBC-based UserDetailsService. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The bean ID of the DataSource which provides the required tables. + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + An SQL statement to query a username, password, and enabled status given a username. + Default is "select username,password,enabled from users where username = ?" + + + + + + An SQL statement to query for a user's granted authorities given a username. The default + is "select username, authority from authorities where username = ?" + + + + + + An SQL statement to query user's group authorities given a username. The default is + "select g.id, g.group_name, ga.authority from groups g, group_members gm, + group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + Element for configuration of the CsrfFilter for protection against CSRF. It also updates + the default RequestCache to only replay "GET" requests. + + + + + + + + + + Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is + enabled). + + + + + + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is + any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + + + + + + The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by + LazyCsrfTokenRepository. + + + + + + + Element for configuration of the HeaderWritersFilter. Enables easy setting for the + X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies if the default headers should be disabled. Default false. + + + + + + Specifies if headers should be disabled. Default false. + + + + + + + Adds support for HTTP Strict Transport Security (HSTS) + + + + + + + + + + Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default true. + + + + + + Specifies the maximum amount of time the host should be considered a Known HSTS Host. + Default one year. + + + + + + The RequestMatcher instance to be used to determine if the header should be set. Default + is if HttpServletRequest.isSecure() is true. + + + + + + Specifies if preload should be included. Default false. + + + + + + + Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is + specified a HandlerMappingIntrospector is used as the CorsConfigurationSource + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to + use + + + + + + + Adds support for HTTP Public Key Pinning (HPKP). + + + + + + + + + + + + + + + + + + The list with pins + + + + + + + + + + + A pin is specified using the base64-encoded SPKI fingerprint as value and the + cryptographic hash algorithm as attribute + + + + + + The cryptographic hash algorithm + + + + + + + + + Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default false. + + + + + + Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + + + + + + Specifies if the browser should only report pin validation failures. Default true. + + + + + + Specifies the URI to which the browser should report pin validation failures. + + + + + + + Adds support for Content Security Policy (CSP) + + + + + + + + + + The security policy directive(s) for the Content-Security-Policy header or if report-only + is set to true, then the Content-Security-Policy-Report-Only header is used. + + + + + + Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy + violations only. Defaults to false. + + + + + + + Adds support for Referrer Policy + + + + + + + + + + The policies for the Referrer-Policy header. + + + + + + + + + + + + + + + + + + + Adds support for Feature Policy + + + + + + + + + + The security policy directive(s) for the Feature-Policy header. + + + + + + + Adds support for Permissions Policy + + + + + + + + + + The policies for the Permissions-Policy header. + + + + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for + every request + + + + + + + + + + Specifies if Cache Control should be disabled. Default false. + + + + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options + header. + + + + + + + + + + If disabled, the X-Frame-Options header will not be included. Default false. + + + + + + Specify the policy to use for the X-Frame-Options-Header. + + + + + + + + + + + + + Specify the strategy to use when ALLOW-FROM is chosen. + + + + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specify a value to use for the chosen strategy. + + + + + + Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' + based strategy. Default is 'from'. Deprecated ALLOW-FROM is an obsolete directive that no + longer works in modern browsers. Instead use Content-Security-Policy with the <a + href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors">frame-ancestors</a> + directive. + + + + + + + Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the + X-XSS-Protection header. + + + + + + + + + + disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + + + + + + specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' + meaning it is enabled. + + + + + + Add mode=block to the header or not, default is on. + + + + + + + Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + + + + + + + + + + If disabled, the X-Content-Type-Options header will not be included. Default false. + + + + + + + Adds support for Cross-Origin-Opener-Policy header + + + + + + + + + + The policies for the Cross-Origin-Opener-Policy header. + + + + + + + + + + + + + + Adds support for Cross-Origin-Embedder-Policy header + + + + + + + + + + The policies for the Cross-Origin-Embedder-Policy header. + + + + + + + + + + + + + Adds support for Cross-Origin-Resource-Policy header + + + + + + + + + + The policies for the Cross-Origin-Resource-Policy header. + + + + + + + + + + + + + + Add additional headers to the response. + + + + + + + + + + The name of the header to add. + + + + + + The value for the header. + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Used to indicate that a filter bean declaration should be incorporated into the security + filter chain. + + + + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/main/resources/org/springframework/security/config/spring-security.xsl b/config/src/main/resources/org/springframework/security/config/spring-security.xsl index 46b9e89acc0..77ee9251486 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security.xsl +++ b/config/src/main/resources/org/springframework/security/config/spring-security.xsl @@ -9,7 +9,7 @@ - ,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,add-headers, + ,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,saml2-login,saml2-logout,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,add-headers, diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java index ddf6c5d9301..1565ea5c90b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -93,8 +93,8 @@ public void customAuthenticationEventPublisherWithWeb() throws Exception { given(opp.postProcess(any())).willAnswer((a) -> a.getArgument(0)); AuthenticationManager am = new AuthenticationManagerBuilder(opp).authenticationEventPublisher(aep) .inMemoryAuthentication().and().build(); - assertThatExceptionOfType(AuthenticationException.class) - .isThrownBy(() -> am.authenticate(new UsernamePasswordAuthenticationToken("user", "password"))); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy( + () -> am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password"))); verify(aep).publishAuthenticationFailure(any(), any()); } @@ -103,7 +103,8 @@ public void getAuthenticationManagerWhenGlobalPasswordEncoderBeanThenUsed() thro this.spring.register(PasswordEncoderGlobalConfig.class).autowire(); AuthenticationManager manager = this.spring.getContext().getBean(AuthenticationConfiguration.class) .getAuthenticationManager(); - Authentication auth = manager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + Authentication auth = manager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); assertThat(auth.getName()).isEqualTo("user"); assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority).containsOnly("ROLE_USER"); } @@ -113,7 +114,8 @@ public void getAuthenticationManagerWhenProtectedPasswordEncoderBeanThenUsed() t this.spring.register(PasswordEncoderGlobalConfig.class).autowire(); AuthenticationManager manager = this.spring.getContext().getBean(AuthenticationConfiguration.class) .getAuthenticationManager(); - Authentication auth = manager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + Authentication auth = manager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); assertThat(auth.getName()).isEqualTo("user"); assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority).containsOnly("ROLE_USER"); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationPublishTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationPublishTests.java index c313502b369..fc0931cf96a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationPublishTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationPublishTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -47,7 +47,8 @@ public class AuthenticationConfigurationPublishTests { // gh-4940 @Test public void authenticationEventPublisherBeanUsedByDefault() { - this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + this.authenticationManager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); assertThat(this.listener.getEvents()).hasSize(1); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java index 412768d1241..243bb0284e6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -129,7 +129,8 @@ public void getAuthenticationManagerWhenNoOpGlobalAuthenticationConfigurerAdapte @Test public void getAuthenticationWhenGlobalAuthenticationConfigurerAdapterThenAuthenticates() throws Exception { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class, UserGlobalAuthenticationConfigurerAdapter.class).autowire(); AuthenticationManager authentication = this.spring.getContext().getBean(AuthenticationConfiguration.class) @@ -139,7 +140,8 @@ public void getAuthenticationWhenGlobalAuthenticationConfigurerAdapterThenAuthen @Test public void getAuthenticationWhenAuthenticationManagerBeanThenAuthenticates() throws Exception { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class, AuthenticationManagerBeanConfig.class).autowire(); AuthenticationManager authentication = this.spring.getContext().getBean(AuthenticationConfiguration.class) @@ -165,9 +167,9 @@ public void getAuthenticationWhenConfiguredThenBootNotTrigger() throws Exception config.setGlobalAuthenticationConfigurers(Arrays.asList(new ConfiguresInMemoryConfigurerAdapter(), new BootGlobalAuthenticationConfigurerAdapter())); AuthenticationManager authenticationManager = config.getAuthenticationManager(); - authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); - assertThatExceptionOfType(AuthenticationException.class).isThrownBy( - () -> authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("boot", "password"))); + authenticationManager.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> authenticationManager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("boot", "password"))); } @Test @@ -176,7 +178,7 @@ public void getAuthenticationWhenNotConfiguredThenBootTrigger() throws Exception AuthenticationConfiguration config = this.spring.getContext().getBean(AuthenticationConfiguration.class); config.setGlobalAuthenticationConfigurers(Arrays.asList(new BootGlobalAuthenticationConfigurerAdapter())); AuthenticationManager authenticationManager = config.getAuthenticationManager(); - authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("boot", "password")); + authenticationManager.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("boot", "password")); } // gh-2531 @@ -206,9 +208,9 @@ public void getAuthenticationWhenUserDetailsServiceBeanThenAuthenticationManager AuthenticationManager am = this.spring.getContext().getBean(AuthenticationConfiguration.class) .getAuthenticationManager(); given(uds.loadUserByUsername("user")).willReturn(PasswordEncodedUser.user(), PasswordEncodedUser.user()); - am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); - assertThatExceptionOfType(AuthenticationException.class) - .isThrownBy(() -> am.authenticate(new UsernamePasswordAuthenticationToken("user", "invalid"))); + am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy( + () -> am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "invalid"))); } @Test @@ -221,9 +223,9 @@ public void getAuthenticationWhenUserDetailsServiceAndPasswordEncoderBeanThenEnc .getAuthenticationManager(); given(uds.loadUserByUsername("user")).willReturn(User.withUserDetails(user).build(), User.withUserDetails(user).build()); - am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); - assertThatExceptionOfType(AuthenticationException.class) - .isThrownBy(() -> am.authenticate(new UsernamePasswordAuthenticationToken("user", "invalid"))); + am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy( + () -> am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "invalid"))); } @Test @@ -237,7 +239,7 @@ public void getAuthenticationWhenUserDetailsServiceAndPasswordManagerThenManager given(manager.loadUserByUsername("user")).willReturn(User.withUserDetails(user).build(), User.withUserDetails(user).build()); given(manager.updatePassword(any(), any())).willReturn(user); - am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); verify(manager).updatePassword(eq(user), startsWith("{bcrypt}")); } @@ -250,7 +252,7 @@ public void getAuthenticationWhenAuthenticationProviderAndUserDetailsBeanThenAut .getAuthenticationManager(); given(ap.supports(any())).willReturn(true); given(ap.authenticate(any())).willReturn(TestAuthentication.authenticatedUser()); - am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); } // gh-3091 @@ -262,7 +264,7 @@ public void getAuthenticationWhenAuthenticationProviderBeanThenUsed() throws Exc .getAuthenticationManager(); given(ap.supports(any())).willReturn(true); given(ap.authenticate(any())).willReturn(TestAuthentication.authenticatedUser()); - am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + am.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/issue50/Issue50Tests.java b/config/src/test/java/org/springframework/security/config/annotation/issue50/Issue50Tests.java index 6de1764992b..668f45f2128 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/issue50/Issue50Tests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/issue50/Issue50Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -75,21 +75,21 @@ public void loadWhenGlobalMethodSecurityConfigurationThenAuthenticationManagerLa @Test public void authenticateWhenMissingUserThenUsernameNotFoundException() { assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(() -> this.authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("test", "password"))); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("test", "password"))); } @Test public void authenticateWhenInvalidPasswordThenBadCredentialsException() { this.userRepo.save(User.withUsernameAndPassword("test", "password")); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("test", "invalid"))); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("test", "invalid"))); } @Test public void authenticateWhenValidUserThenAuthenticates() { this.userRepo.save(User.withUsernameAndPassword("test", "password")); Authentication result = this.authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("test", "password")); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("test", "password")); assertThat(result.getName()).isEqualTo("test"); } @@ -98,7 +98,7 @@ public void globalMethodSecurityIsEnabledWhenNotAllowedThenAccessDenied() { SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("test", null, "ROLE_USER")); this.userRepo.save(User.withUsernameAndPassword("denied", "password")); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("test", "password"))); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("test", "password"))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.java index 2a0ae08bbe2..b7f0a2cf85f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -106,8 +106,8 @@ public void configureWhenGlobalMethodSecurityHasCustomMetadataSourceThenNoEnabli @Test public void methodSecurityAuthenticationManagerPublishesEvent() { this.spring.register(InMemoryAuthWithGlobalMethodSecurityConfig.class).autowire(); - assertThatExceptionOfType(AuthenticationException.class).isThrownBy( - () -> this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("foo", "bar"))); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> this.authenticationManager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("foo", "bar"))); assertThat(this.events.getEvents()).extracting(Object::getClass) .containsOnly((Class) AuthenticationFailureBadCredentialsEvent.class); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 4be39df6ea5..4af1f89cf6e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Supplier; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -32,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.AccessDeniedException; @@ -43,9 +45,11 @@ import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -58,6 +62,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link PrePostMethodSecurityConfiguration}. @@ -350,6 +357,27 @@ public void repeatedSecuredAnnotationsWhenPresentThenFails() { .isThrownBy(() -> this.businessService.repeatedAnnotations()); } + @WithMockUser + @Test + public void preAuthorizeWhenAuthorizationEventPublisherThenUses() { + this.spring.register(MethodSecurityServiceConfig.class, AuthorizationEventPublisherConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.methodSecurityService.preAuthorize()); + AuthorizationEventPublisher publisher = this.spring.getContext().getBean(AuthorizationEventPublisher.class); + verify(publisher).publishAuthorizationEvent(any(Supplier.class), any(MethodInvocation.class), + any(AuthorizationDecision.class)); + } + + @WithMockUser + @Test + public void postAuthorizeWhenAuthorizationEventPublisherThenUses() { + this.spring.register(MethodSecurityServiceConfig.class, AuthorizationEventPublisherConfig.class).autowire(); + this.methodSecurityService.postAnnotation("grant"); + AuthorizationEventPublisher publisher = this.spring.getContext().getBean(AuthorizationEventPublisher.class); + verify(publisher).publishAuthorizationEvent(any(Supplier.class), any(MethodInvocationResult.class), + any(AuthorizationDecision.class)); + } + // gh-10305 @WithMockUser @Test @@ -484,4 +512,16 @@ Advisor customAfterAdvice() { } + @Configuration + static class AuthorizationEventPublisherConfig { + + private final AuthorizationEventPublisher publisher = mock(AuthorizationEventPublisher.class); + + @Bean + AuthorizationEventPublisher authorizationEventPublisher() { + return this.publisher; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistrationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistrationTests.java index 3991a622cf7..19632cbb32a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistrationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistrationTests.java @@ -53,7 +53,7 @@ public void putWhenCustomFilterAlreadyExistsThenDoesNotOverride() { @Test public void putWhenPredefinedFilterThenDoesNotOverride() { - int position = 100; + int position = 300; Integer predefinedFilterOrderBefore = this.filterOrderRegistration.getOrder(ChannelProcessingFilter.class); this.filterOrderRegistration.put(MyFilter.class, position); Integer myFilterOrder = this.filterOrderRegistration.getOrder(MyFilter.class); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java index 275c5d23c9b..9eb0abc7a66 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java @@ -16,6 +16,9 @@ package org.springframework.security.config.annotation.web.builders; +import java.io.IOException; + +import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; @@ -24,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -32,6 +36,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -92,6 +97,15 @@ public void ignoringMvcMatcher() throws Exception { assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); } + @Test + public void requestRejectedHandlerInvoked() throws ServletException, IOException { + loadConfig(RequestRejectedHandlerConfig.class); + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/\u0019path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); + } + @Test public void ignoringMvcMatcherServletPath() throws Exception { loadConfig(MvcMatcherServletPathConfig.class, LegacyMvcMatchingConfig.class); @@ -223,4 +237,14 @@ public void configurePathMatch(PathMatchConfigurer configurer) { } + @EnableWebSecurity + static class RequestRejectedHandlerConfig extends WebSecurityConfigurerAdapter { + + @Override + public void configure(WebSecurity web) throws Exception { + web.requestRejectedHandler(new HttpStatusRequestRejectedHandler(HttpStatus.BAD_REQUEST.value())); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java index 0899165c924..2272e476df1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -65,7 +65,7 @@ public void authenticationPrincipalExpressionWhenBeanExpressionSuppliedThenBeanU User user = new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER")); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication( - new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())); + UsernamePasswordAuthenticationToken.authenticated(user, user.getPassword(), user.getAuthorities())); SecurityContextHolder.setContext(context); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); // @formatter:off diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.java index dd05d35cc08..0f50a172faf 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.java @@ -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. @@ -60,7 +60,7 @@ public void configureWhenOverrideAuthenticationManagerBeanThenAuthenticationMana this.spring.register(SecurityConfig.class).autowire(); AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); Authentication authentication = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); assertThat(authentication.isAuthenticated()).isTrue(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java index 813723e2839..030f8f2a7ea 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java @@ -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. @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configuration; +import java.util.Arrays; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; @@ -23,14 +24,20 @@ import com.google.common.net.HttpHeaders; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.context.SecurityContextHolder; @@ -47,6 +54,7 @@ import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -65,7 +73,7 @@ * * @author Eleftheria Stein */ -@ExtendWith(SpringTestContextExtension.class) +@ExtendWith({ MockitoExtension.class, SpringTestContextExtension.class }) public class HttpSecurityConfigurationTests { public final SpringTestContext spring = new SpringTestContext(this); @@ -73,6 +81,9 @@ public class HttpSecurityConfigurationTests { @Autowired private MockMvc mockMvc; + @Mock + private MockedStatic springFactoriesLoader; + @Test public void postWhenDefaultFilterChainBeanThenRespondsWithForbidden() throws Exception { this.spring.register(DefaultWithFilterChainConfig.class).autowire(); @@ -200,6 +211,35 @@ public void loginWhenUsingDefaultsThenDefaultLogoutSuccessPageGenerated() throws this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk()); } + @Test + public void configureWhenAuthorizeHttpRequestsBeforeAuthorizeRequestThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy( + () -> this.spring.register(AuthorizeHttpRequestsBeforeAuthorizeRequestsConfig.class).autowire()) + .withMessageContaining( + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."); + } + + @Test + public void configureWhenAuthorizeHttpRequestsAfterAuthorizeRequestThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy( + () -> this.spring.register(AuthorizeHttpRequestsAfterAuthorizeRequestsConfig.class).autowire()) + .withMessageContaining( + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."); + } + + @Test + public void configureWhenDefaultConfigurerAsSpringFactoryThenDefaultConfigurerApplied() { + DefaultConfigurer configurer = new DefaultConfigurer(); + this.springFactoriesLoader.when( + () -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader())) + .thenReturn(Arrays.asList(configurer)); + this.spring.register(DefaultWithFilterChainConfig.class).autowire(); + assertThat(configurer.init).isTrue(); + assertThat(configurer.configure).isTrue(); + } + @RestController static class NameController { @@ -270,6 +310,44 @@ UserDetailsService userDetailsService() { } + @EnableWebSecurity + static class AuthorizeHttpRequestsBeforeAuthorizeRequestsConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .authorizeRequests((requests) -> requests + .anyRequest().authenticated() + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class AuthorizeHttpRequestsAfterAuthorizeRequestsConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeRequests((requests) -> requests + .anyRequest().authenticated() + ) + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .build(); + // @formatter:on + } + + } + @RestController static class BaseController { @@ -291,4 +369,22 @@ void user(HttpServletRequest request) { } + static class DefaultConfigurer extends AbstractHttpConfigurer { + + boolean init; + + boolean configure; + + @Override + public void init(HttpSecurity builder) { + this.init = true; + } + + @Override + public void configure(HttpSecurity builder) { + this.configure = true; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index ce9977c7c5d..1f0b876d272 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -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. @@ -33,6 +33,7 @@ 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.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 @@ * @author Rob Winch * @author Joe Grandja * @author Evgeniy Cheban + * @author Marcus Da Coregio */ @ExtendWith(SpringTestContextExtension.class) public class WebSecurityConfigurationTests { @@ -218,10 +220,10 @@ public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEv } @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 void loadConfigWhenSecurityFilterChainBeanThenDefaultWebInvocationPrivile 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 void loadConfigWhenMultipleAuthenticationManagersAndWebSecurityConfigurer 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 { @@ -948,7 +1013,7 @@ static AuthenticationManager authenticationManager1() { return new ProviderManager(new AuthenticationProvider() { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - return new UsernamePasswordAuthenticationToken("user", "credentials"); + return UsernamePasswordAuthenticationToken.unauthenticated("user", "credentials"); } @Override @@ -963,7 +1028,7 @@ static AuthenticationManager authenticationManager2() { return new ProviderManager(new AuthenticationProvider() { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - return new UsernamePasswordAuthenticationToken("subuser", "credentials"); + return UsernamePasswordAuthenticationToken.unauthenticated("subuser", "credentials"); } @Override @@ -1008,4 +1073,125 @@ protected AuthenticationManager authenticationManager() { } + @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(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 29e097e5800..81fcccbb12c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,19 @@ package org.springframework.security.config.annotation.web.configurers; +import java.util.function.Supplier; + +import javax.servlet.http.HttpServletRequest; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; @@ -31,13 +38,17 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -129,9 +140,9 @@ public void configureMvcMatcherAccessAuthorizationManagerWhenNullThenException() @Test public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - verify(ObjectPostProcessorConfig.objectPostProcessor) - .postProcess(any(RequestMatcherDelegatingAuthorizationManager.class)); - verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(AuthorizationFilter.class)); + ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class); + verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class)); + verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class)); } @Test @@ -369,6 +380,15 @@ public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUna this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); } + @Test + public void getWhenCustomAuthorizationEventPublisherThenUses() throws Exception { + this.spring.register(AuthenticatedConfig.class, AuthorizationEventPublisherConfig.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + AuthorizationEventPublisher publisher = this.spring.getContext().getBean(AuthorizationEventPublisher.class); + verify(publisher).publishAuthorizationEvent(any(Supplier.class), any(HttpServletRequest.class), + any(AuthorizationDecision.class)); + } + @Test public void getWhenAnyRequestAuthenticatedConfiguredAndUserLoggedInThenRespondsWithOk() throws Exception { this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire(); @@ -380,6 +400,99 @@ public void getWhenAnyRequestAuthenticatedConfiguredAndUserLoggedInThenRespondsW this.mvc.perform(requestWithUser).andExpect(status().isOk()); } + @Test + public void getWhenExpressionHasRoleUserConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception { + this.spring.register(ExpressionRoleUserConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenExpressionHasRoleUserConfiguredAndRoleIsAdminThenRespondsWithForbidden() throws Exception { + this.spring.register(ExpressionRoleUserConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .roles("ADMIN")); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isForbidden()); + } + + @Test + public void getWhenExpressionRoleUserOrAdminConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception { + this.spring.register(ExpressionRoleUserOrAdminConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenExpressionRoleUserOrAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception { + this.spring.register(ExpressionRoleUserOrAdminConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .roles("ADMIN")); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); + } + + @Test + public void getWhenExpressionRoleUserOrAdminConfiguredAndRoleIsOtherThenRespondsWithForbidden() throws Exception { + this.spring.register(ExpressionRoleUserOrAdminConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithRoleOther = get("/") + .with(user("user") + .roles("OTHER")); + // @formatter:on + this.mvc.perform(requestWithRoleOther).andExpect(status().isForbidden()); + } + + @Test + public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk() + throws Exception { + this.spring.register(ExpressionIpAddressLocalhostConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestFromLocalhost = get("/") + .with(remoteAddress("127.0.0.1")); + // @formatter:on + this.mvc.perform(requestFromLocalhost).andExpect(status().isOk()); + } + + @Test + public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsOtherThenRespondsWithForbidden() + throws Exception { + this.spring.register(ExpressionIpAddressLocalhostConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestFromOtherHost = get("/") + .with(remoteAddress("192.168.0.1")); + // @formatter:on + this.mvc.perform(requestFromOtherHost).andExpect(status().isForbidden()); + } + + @Test + public void requestWhenMvcMatcherPathVariablesThenMatchesOnPathVariables() throws Exception { + this.spring.register(MvcMatcherPathVariablesInLambdaConfig.class).autowire(); + MockHttpServletRequestBuilder request = get("/user/user"); + this.mvc.perform(request).andExpect(status().isOk()); + request = get("/user/deny"); + this.mvc.perform(request).andExpect(status().isUnauthorized()); + } + + private static RequestPostProcessor remoteAddress(String remoteAddress) { + return (request) -> { + request.setRemoteAddr(remoteAddress); + return request; + }; + } + @EnableWebSecurity static class NoRequestsConfig { @@ -495,7 +608,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @EnableWebSecurity static class ObjectPostProcessorConfig { - static ObjectPostProcessor objectPostProcessor = spy(ReflectingObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = spy(ReflectingObjectPostProcessor.class); @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -509,8 +622,8 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } @Bean - static ObjectPostProcessor objectPostProcessor() { - return objectPostProcessor; + ObjectPostProcessor objectPostProcessor() { + return this.objectPostProcessor; } } @@ -698,6 +811,95 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } + @EnableWebSecurity + static class ExpressionRoleUserConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().access(new WebExpressionAuthorizationManager("hasRole('USER')")) + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class ExpressionRoleUserOrAdminConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().access(new WebExpressionAuthorizationManager("hasRole('USER') or hasRole('ADMIN')")) + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class ExpressionIpAddressLocalhostConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().access(new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1')")) + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MvcMatcherPathVariablesInLambdaConfig { + + @Bean + SecurityFilterChain chain(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(withDefaults()) + .authorizeHttpRequests((requests) -> requests + .mvcMatchers("/user/{username}").access(new WebExpressionAuthorizationManager("#username == 'user'")) + ); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping("/user/{username}") + String path(@PathVariable("username") String username) { + return username; + } + + } + + } + + @Configuration + static class AuthorizationEventPublisherConfig { + + private final AuthorizationEventPublisher publisher = mock(AuthorizationEventPublisher.class); + + @Bean + AuthorizationEventPublisher authorizationEventPublisher() { + return this.publisher; + } + + } + @RestController static class BasicController { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index cc8c9dc8856..2fffb4f2a3f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -150,7 +150,7 @@ public void antMatchersPathVariablesCaseInsensitiveCamelCaseVariables() throws E public void roleHiearchy() throws Exception { loadConfig(RoleHiearchyConfig.class); SecurityContext securityContext = new SecurityContextImpl(); - securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("test", "notused", + securityContext.setAuthentication(UsernamePasswordAuthenticationToken.authenticated("test", "notused", AuthorityUtils.createAuthorityList("ROLE_USER"))); this.request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index 041419e50d9..ad592b1c716 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,33 @@ package org.springframework.security.config.annotation.web.configurers; +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.PortMapperImpl; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.access.channel.InsecureChannelProcessor; import org.springframework.security.web.access.channel.SecureChannelProcessor; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; @@ -44,6 +55,7 @@ * * @author Rob Winch * @author Eleftheria Stein + * @author Onur Kagan Ozcan */ @ExtendWith(SpringTestContextExtension.class) public class ChannelSecurityConfigurerTests { @@ -93,6 +105,30 @@ public void requestWhenRequiresChannelConfiguredInLambdaThenRedirectsToHttps() t this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/")); } + @Test + public void requestWhenRequiresChannelConfiguredWithUrlRedirectThenRedirectsToUrlWithHttps() throws Exception { + this.spring.register(RequiresChannelWithTestUrlRedirectStrategy.class).autowire(); + this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/test")); + } + + // gh-10956 + @Test + public void requestWhenRequiresChannelWithMultiMvcMatchersThenRedirectsToHttps() throws Exception { + this.spring.register(RequiresChannelMultiMvcMatchersConfig.class).autowire(); + this.mvc.perform(get("/test-1")).andExpect(redirectedUrl("https://localhost/test-1")); + this.mvc.perform(get("/test-2")).andExpect(redirectedUrl("https://localhost/test-2")); + this.mvc.perform(get("/test-3")).andExpect(redirectedUrl("https://localhost/test-3")); + } + + // gh-10956 + @Test + public void requestWhenRequiresChannelWithMultiMvcMatchersInLambdaThenRedirectsToHttps() throws Exception { + this.spring.register(RequiresChannelMultiMvcMatchersInLambdaConfig.class).autowire(); + this.mvc.perform(get("/test-1")).andExpect(redirectedUrl("https://localhost/test-1")); + this.mvc.perform(get("/test-2")).andExpect(redirectedUrl("https://localhost/test-2")); + this.mvc.perform(get("/test-3")).andExpect(redirectedUrl("https://localhost/test-3")); + } + @EnableWebSecurity static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter { @@ -155,4 +191,90 @@ protected void configure(HttpSecurity http) throws Exception { } + @EnableWebSecurity + static class RequiresChannelWithTestUrlRedirectStrategy extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper() + .portMapper(new PortMapperImpl()) + .and() + .requiresChannel() + .redirectStrategy(new TestUrlRedirectStrategy()) + .anyRequest() + .requiresSecure(); + // @formatter:on + } + + } + + static class TestUrlRedirectStrategy implements RedirectStrategy { + + @Override + public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) + throws IOException { + String redirectUrl = url + "test"; + redirectUrl = response.encodeRedirectURL(redirectUrl); + response.sendRedirect(redirectUrl); + } + + } + + @EnableWebSecurity + @EnableWebMvc + static class RequiresChannelMultiMvcMatchersConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper() + .portMapper(new PortMapperImpl()) + .and() + .requiresChannel() + .mvcMatchers("/test-1") + .requiresSecure() + .mvcMatchers("/test-2") + .requiresSecure() + .mvcMatchers("/test-3") + .requiresSecure() + .anyRequest() + .requiresInsecure(); + // @formatter:on + return http.build(); + } + + } + + @EnableWebSecurity + @EnableWebMvc + static class RequiresChannelMultiMvcMatchersInLambdaConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper((port) -> port + .portMapper(new PortMapperImpl()) + ) + .requiresChannel((channel) -> channel + .mvcMatchers("/test-1") + .requiresSecure() + .mvcMatchers("/test-2") + .requiresSecure() + .mvcMatchers("/test-3") + .requiresSecure() + .anyRequest() + .requiresInsecure() + ); + // @formatter:on + return http.build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java index 750609bf149..bc431b6afbb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java @@ -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. @@ -320,6 +320,17 @@ public void getWhenCustomAccessDeniedHandlerThenHandlerIsUsed() throws Exception any(HttpServletResponse.class), any()); } + @Test + public void getWhenCustomDefaultAccessDeniedHandlerForThenHandlerIsUsed() throws Exception { + DefaultAccessDeniedHandlerForConfig.DENIED_HANDLER = mock(AccessDeniedHandler.class); + DefaultAccessDeniedHandlerForConfig.MATCHER = mock(RequestMatcher.class); + given(DefaultAccessDeniedHandlerForConfig.MATCHER.matches(any())).willReturn(true); + this.spring.register(DefaultAccessDeniedHandlerForConfig.class, BasicController.class).autowire(); + this.mvc.perform(post("/")).andExpect(status().isOk()); + verify(DefaultAccessDeniedHandlerForConfig.DENIED_HANDLER).handle(any(HttpServletRequest.class), + any(HttpServletResponse.class), any()); + } + @Test public void loginWhenNoCsrfTokenThenRespondsWithForbidden() throws Exception { this.spring.register(FormLoginConfig.class).autowire(); @@ -608,6 +619,24 @@ protected void configure(HttpSecurity http) throws Exception { } + @EnableWebSecurity + static class DefaultAccessDeniedHandlerForConfig extends WebSecurityConfigurerAdapter { + + static AccessDeniedHandler DENIED_HANDLER; + + static RequestMatcher MATCHER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .exceptionHandling() + .defaultAccessDeniedHandlerFor(DENIED_HANDLER, MATCHER); + // @formatter:on + } + + } + @EnableWebSecurity static class FormLoginConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java index cc64f4b92a9..2b2f2a0b55c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,16 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode; import org.springframework.test.web.servlet.MockMvc; @@ -52,6 +57,7 @@ * @author Eddú Meléndez * @author Vedran Pavic * @author Eleftheria Stein + * @author Marcus Da Coregio */ @ExtendWith(SpringTestContextExtension.class) public class HeadersConfigurerTests { @@ -514,6 +520,30 @@ public void getWhenHstsConfiguredWithPreloadInLambdaThenStrictTransportSecurityH assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.STRICT_TRANSPORT_SECURITY); } + @Test + public void getWhenCustomCrossOriginPoliciesInLambdaThenCrossOriginPolicyHeadersWithCustomValuesInResponse() + throws Exception { + this.spring.register(CrossOriginCustomPoliciesInLambdaConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(get("/")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, "same-origin")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, "require-corp")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY, "same-origin")).andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, + HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY); + } + + @Test + public void getWhenCustomCrossOriginPoliciesThenCrossOriginPolicyHeadersWithCustomValuesInResponse() + throws Exception { + this.spring.register(CrossOriginCustomPoliciesConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(get("/")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, "same-origin")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, "require-corp")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY, "same-origin")).andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, + HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY); + } + @EnableWebSecurity static class HeadersConfig extends WebSecurityConfigurerAdapter { @@ -1146,4 +1176,50 @@ protected void configure(HttpSecurity http) throws Exception { } + @EnableWebSecurity + static class CrossOriginCustomPoliciesInLambdaConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http.headers((headers) -> headers + .defaultsDisabled() + .crossOriginOpenerPolicy((policy) -> policy + .policy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN) + ) + .crossOriginEmbedderPolicy((policy) -> policy + .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + ) + .crossOriginResourcePolicy((policy) -> policy + .policy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN) + ) + ); + // @formatter:on + return http.build(); + } + + } + + @EnableWebSecurity + static class CrossOriginCustomPoliciesConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http.headers() + .defaultsDisabled() + .crossOriginOpenerPolicy() + .policy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN) + .and() + .crossOriginEmbedderPolicy() + .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + .and() + .crossOriginResourcePolicy() + .policy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + // @formatter:on + return http.build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java index 58f83e9d085..99594611dcb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -33,13 +34,19 @@ import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -48,6 +55,7 @@ import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -118,6 +126,13 @@ public void httpBasicWhenRememberMeConfiguredThenSetsRememberMeCookie() throws E this.mvc.perform(rememberMeRequest).andExpect(cookie().exists("remember-me")); } + @Test + public void httpBasicWhenDefaultsThenAcceptsBasicCredentials() throws Exception { + this.spring.register(HttpBasic.class, Users.class, Home.class).autowire(); + this.mvc.perform(get("/").with(httpBasic("user", "password"))).andExpect(status().isOk()) + .andExpect(content().string("user")); + } + @EnableWebSecurity static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter { @@ -269,6 +284,37 @@ protected void configure(HttpSecurity http) throws Exception { @Override @Bean public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + // @formatter:off + org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + // @formatter:on + ); + } + + } + + @EnableWebSecurity + static class HttpBasic { + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .httpBasic(Customizer.withDefaults()); + + return http.build(); + } + + } + + @Configuration + static class Users { + + @Bean + UserDetailsService userDetailsService() { return new InMemoryUserDetailsManager( // @formatter:off User.withDefaultPasswordEncoder() @@ -282,4 +328,15 @@ public UserDetailsService userDetailsService() { } + @EnableWebMvc + @RestController + static class Home { + + @GetMapping("/") + String home(@AuthenticationPrincipal UserDetails user) { + return user.getUsername(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index 9e60e93994f..da67b591fb1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -23,7 +23,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -33,6 +36,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -167,6 +171,38 @@ public void requestMatcherWhensMvcMatcherServletPathInLambdaThenPathIsSecured() assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); } + @Test + public void requestMatcherWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception { + loadConfig(MultiMvcMatcherInLambdaConfig.class); + this.request.setRequestURI("/test-1"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-2"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-3"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + public void requestMatcherWhenMultiMvcMatcherThenAllPathsAreDenied() throws Exception { + loadConfig(MultiMvcMatcherConfig.class); + this.request.setRequestURI("/test-1"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-2"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-3"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); @@ -175,6 +211,101 @@ public void loadConfig(Class... configs) { this.context.getAutowireCapableBeanFactory().autowireBean(this); } + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MultiMvcMatcherInLambdaConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain first(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests + .mvcMatchers("/test-1") + .mvcMatchers("/test-2") + .mvcMatchers("/test-3") + ) + .authorizeRequests((authorize) -> authorize.anyRequest().denyAll()) + .httpBasic(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + SecurityFilterChain second(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests + .mvcMatchers("/test-1") + ) + .authorizeRequests((authorize) -> authorize + .anyRequest().permitAll() + ); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping({ "/test-1", "/test-2", "/test-3" }) + String path() { + return "path"; + } + + } + + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MultiMvcMatcherConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain first(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers() + .mvcMatchers("/test-1") + .mvcMatchers("/test-2") + .mvcMatchers("/test-3") + .and() + .authorizeRequests() + .anyRequest().denyAll() + .and() + .httpBasic(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + SecurityFilterChain second(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers() + .mvcMatchers("/test-1") + .and() + .authorizeRequests() + .anyRequest().permitAll(); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping({ "/test-1", "/test-2", "/test-3" }) + String path() { + return "path"; + } + + } + + } + @EnableWebSecurity @Configuration @EnableWebMvc diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.java index 6f5c5aec5d6..4301210456e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -100,7 +100,8 @@ public void requestWhenRequiresChannelThenBehaviorMatchesNamespace() throws Exce } private static Authentication user(String role) { - return new UsernamePasswordAuthenticationToken("user", null, AuthorityUtils.createAuthorityList(role)); + return UsernamePasswordAuthenticationToken.authenticated("user", null, + AuthorityUtils.createAuthorityList(role)); } @EnableWebSecurity diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java index 7e2cfa0e413..3801a10ed90 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -97,7 +97,7 @@ public void requestWhenCustomAccessDeniedHandlerInLambdaThenBehaviorMatchesNames } private static Authentication user() { - return new UsernamePasswordAuthenticationToken("user", null, AuthorityUtils.NO_AUTHORITIES); + return UsernamePasswordAuthenticationToken.authenticated("user", null, AuthorityUtils.NO_AUTHORITIES); } private T verifyBean(Class beanClass) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java index 70752fb1c5f..9a5174f810c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java @@ -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. @@ -61,11 +61,32 @@ public void performWhenUsingPermitAllExactUrlRequestMatcherThenMatchesExactUrl() this.mvc.perform(getWithCsrf).andExpect(status().isFound()); } + @Test + public void performWhenUsingPermitAllExactUrlRequestMatcherThenMatchesExactUrlWithAuthorizeHttp() throws Exception { + this.spring.register(PermitAllConfigAuthorizeHttpRequests.class).autowire(); + MockHttpServletRequestBuilder request = get("/app/xyz").contextPath("/app"); + this.mvc.perform(request).andExpect(status().isNotFound()); + MockHttpServletRequestBuilder getWithQuery = get("/app/xyz?def").contextPath("/app"); + this.mvc.perform(getWithQuery).andExpect(status().isFound()); + MockHttpServletRequestBuilder postWithQueryAndCsrf = post("/app/abc?def").with(csrf()).contextPath("/app"); + this.mvc.perform(postWithQueryAndCsrf).andExpect(status().isNotFound()); + MockHttpServletRequestBuilder getWithCsrf = get("/app/abc").with(csrf()).contextPath("/app"); + this.mvc.perform(getWithCsrf).andExpect(status().isFound()); + } + @Test public void configureWhenNotAuthorizeRequestsThenException() { assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(NoAuthorizedUrlsConfig.class).autowire()) - .withMessageContaining("permitAll only works with HttpSecurity.authorizeRequests"); + .isThrownBy(() -> this.spring.register(NoAuthorizedUrlsConfig.class).autowire()).withMessageContaining( + "permitAll only works with either HttpSecurity.authorizeRequests() or HttpSecurity.authorizeHttpRequests()"); + } + + @Test + public void configureWhenBothAuthorizeRequestsAndAuthorizeHttpRequestsThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(PermitAllConfigWithBothConfigs.class).autowire()) + .withMessageContaining( + "permitAll only works with either HttpSecurity.authorizeRequests() or HttpSecurity.authorizeHttpRequests()"); } @EnableWebSecurity @@ -86,6 +107,45 @@ protected void configure(HttpSecurity http) throws Exception { } + @EnableWebSecurity + static class PermitAllConfigAuthorizeHttpRequests extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/xyz").permitAll() + .loginProcessingUrl("/abc?def").permitAll(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class PermitAllConfigWithBothConfigs extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .authorizeHttpRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/xyz").permitAll() + .loginProcessingUrl("/abc?def").permitAll(); + // @formatter:on + } + + } + @EnableWebSecurity static class NoAuthorizedUrlsConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index aa91f3a8f16..dbeeb2c16ea 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -42,6 +42,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; @@ -117,6 +118,20 @@ public void rememberMeWhenInvokedTwiceThenUsesOriginalUserDetailsService() throw verify(DuplicateDoesNotOverrideConfig.userDetailsService).loadUserByUsername("user"); } + @Test + public void rememberMeWhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception { + this.spring.register(UserDetailsServiceBeanConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(post("/login").with(csrf()).param("username", "user") + .param("password", "password").param("remember-me", "true")).andReturn(); + Cookie rememberMeCookie = mvcResult.getResponse().getCookie("remember-me"); + // @formatter:off + MockHttpServletRequestBuilder request = get("/abc").cookie(rememberMeCookie); + SecurityMockMvcResultMatchers.AuthenticatedMatcher remembermeAuthentication = authenticated() + .withAuthentication((auth) -> assertThat(auth).isInstanceOf(RememberMeAuthenticationToken.class)); + // @formatter:on + this.mvc.perform(request).andExpect(remembermeAuthentication); + } + @Test public void loginWhenRememberMeTrueThenRespondsWithRememberMeCookie() throws Exception { this.spring.register(RememberMeConfig.class).autowire(); @@ -370,6 +385,26 @@ public UserDetailsService userDetailsService() { } + @EnableWebSecurity + static class UserDetailsServiceBeanConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .rememberMe(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService customUserDetailsService() { + return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); + } + + } + @EnableWebSecurity static class RememberMeConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java index bd1f4e937b9..263f3922d4e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web.configurers; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.Filter; import javax.servlet.http.HttpSession; import org.junit.jupiter.api.Test; @@ -33,8 +37,11 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; @@ -74,7 +81,8 @@ public void configureWhenRegisteringObjectPostProcessorThenInvokedOnSecurityCont @Test public void securityContextWhenInvokedTwiceThenUsesOriginalSecurityContextRepository() throws Exception { this.spring.register(DuplicateDoesNotOverrideConfig.class).autowire(); - given(DuplicateDoesNotOverrideConfig.SCR.loadContext(any())).willReturn(mock(SecurityContext.class)); + given(DuplicateDoesNotOverrideConfig.SCR.loadContext(any(HttpRequestResponseHolder.class))) + .willReturn(mock(SecurityContext.class)); this.mvc.perform(get("/")); verify(DuplicateDoesNotOverrideConfig.SCR).loadContext(any(HttpRequestResponseHolder.class)); } @@ -110,6 +118,27 @@ public void requestWhenNullSecurityContextRepositoryInLambdaThenContextNotSavedI assertThat(session).isNull(); } + @Test + public void requireExplicitSave() throws Exception { + HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository(); + SpringTestContext testContext = this.spring.register(RequireExplicitSaveConfig.class); + testContext.autowire(); + FilterChainProxy filterChainProxy = testContext.getContext().getBean(FilterChainProxy.class); + // @formatter:off + List> filterTypes = filterChainProxy.getFilters("/") + .stream() + .map(Filter::getClass) + .collect(Collectors.toList()); + assertThat(filterTypes) + .contains(SecurityContextHolderFilter.class) + .doesNotContain(SecurityContextPersistenceFilter.class); + // @formatter:on + MvcResult mvcResult = this.mvc.perform(formLogin()).andReturn(); + SecurityContext securityContext = repository + .loadContext(new HttpRequestResponseHolder(mvcResult.getRequest(), mvcResult.getResponse())); + assertThat(securityContext.getAuthentication()).isNotNull(); + } + @EnableWebSecurity static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter { @@ -241,14 +270,39 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { @EnableWebSecurity static class NullSecurityContextRepositoryInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .securityContext((securityContext) -> + securityContext + .securityContextRepository(new NullSecurityContextRepository()) + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + + } + + @EnableWebSecurity + static class RequireExplicitSaveConfig extends WebSecurityConfigurerAdapter { + @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http .formLogin(withDefaults()) - .securityContext((securityContext) -> - securityContext - .securityContextRepository(new NullSecurityContextRepository()) + .securityContext((securityContext) -> securityContext + .requireExplicitSave(true) ); // @formatter:on } @@ -258,7 +312,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { // @formatter:off auth .inMemoryAuthentication() - .withUser(PasswordEncodedUser.user()); + .withUser(PasswordEncodedUser.user()); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index 7fdf966b529..ef1645da99f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -38,6 +38,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy; @@ -51,19 +52,26 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -295,6 +303,46 @@ public void whenTwoSessionRegistryBeansThenUseNeither() throws Exception { verifyNoInteractions(SessionRegistryTwoBeansConfig.SESSION_REGISTRY_TWO); } + @Test + public void whenEnableSessionUrlRewritingTrueThenEncodeNotInvoked() throws Exception { + this.spring.register(EnableUrlRewriteConfig.class).autowire(); + // @formatter:off + this.mvc = MockMvcBuilders.webAppContextSetup(this.spring.getContext()) + .addFilters((request, response, chain) -> { + HttpServletResponse responseToSpy = spy((HttpServletResponse) response); + chain.doFilter(request, responseToSpy); + verify(responseToSpy, atLeastOnce()).encodeRedirectURL(any()); + verify(responseToSpy, atLeastOnce()).encodeRedirectUrl(any()); + verify(responseToSpy, atLeastOnce()).encodeURL(any()); + verify(responseToSpy, atLeastOnce()).encodeUrl(any()); + }) + .apply(springSecurity()) + .build(); + // @formatter:on + + this.mvc.perform(get("/")).andExpect(content().string("encoded")); + } + + @Test + public void whenDefaultThenEncodeNotInvoked() throws Exception { + this.spring.register(DefaultUrlRewriteConfig.class).autowire(); + // @formatter:off + this.mvc = MockMvcBuilders.webAppContextSetup(this.spring.getContext()) + .addFilters((request, response, chain) -> { + HttpServletResponse responseToSpy = spy((HttpServletResponse) response); + chain.doFilter(request, responseToSpy); + verify(responseToSpy, never()).encodeRedirectURL(any()); + verify(responseToSpy, never()).encodeRedirectUrl(any()); + verify(responseToSpy, never()).encodeURL(any()); + verify(responseToSpy, never()).encodeUrl(any()); + }) + .apply(springSecurity()) + .build(); + // @formatter:on + + this.mvc.perform(get("/")).andExpect(content().string("encoded")); + } + @EnableWebSecurity static class SessionManagementRequestCacheConfig extends WebSecurityConfigurerAdapter { @@ -569,4 +617,49 @@ SessionRegistry sessionRegistryTwo() { } + @EnableWebSecurity + static class DefaultUrlRewriteConfig { + + @Bean + DefaultSecurityFilterChain configure(HttpSecurity http) throws Exception { + return http.build(); + } + + @Bean + EncodesUrls encodesUrls() { + return new EncodesUrls(); + } + + } + + @EnableWebSecurity + static class EnableUrlRewriteConfig { + + @Bean + DefaultSecurityFilterChain configure(HttpSecurity http) throws Exception { + http.sessionManagement((sessions) -> sessions.enableSessionUrlRewriting(true)); + return http.build(); + } + + @Bean + EncodesUrls encodesUrls() { + return new EncodesUrls(); + } + + } + + @RestController + static class EncodesUrls { + + @RequestMapping("/") + String encoded(HttpServletResponse response) { + response.encodeURL("/foo"); + response.encodeUrl("/foo"); + response.encodeRedirectURL("/foo"); + response.encodeRedirectUrl("/foo"); + return "encoded"; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java index 914ea135ea9..a3bf8574e3a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.configurers; +import java.util.Base64; + import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; @@ -23,16 +25,24 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -125,6 +135,35 @@ public void anonymousUrlAuthorization() { loadConfig(AnonymousUrlAuthorizationConfig.class); } + // gh-10956 + @Test + public void multiMvcMatchersConfig() throws Exception { + loadConfig(MultiMvcMatcherConfig.class); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-1"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + setup(); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-2"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + setup(); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-3"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + setup(); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-x"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); @@ -228,4 +267,41 @@ public void configurePathMatch(PathMatchConfigurer configurer) { } + @EnableWebSecurity + @EnableWebMvc + static class MultiMvcMatcherConfig { + + @Bean + SecurityFilterChain security(HttpSecurity http, ApplicationContext context) throws Exception { + // @formatter:off + http + .httpBasic(Customizer.withDefaults()) + .apply(new UrlAuthorizationConfigurer<>(context)).getRegistry() + .mvcMatchers("/test-1").hasRole("ADMIN") + .mvcMatchers("/test-2").hasRole("ADMIN") + .mvcMatchers("/test-3").hasRole("ADMIN") + .anyRequest().hasRole("USER"); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } + + @RestController + static class PathController { + + @RequestMapping({ "/test-1", "/test-2", "/test-3", "/test-x" }) + String path() { + return "path"; + } + + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index ebcfb0ec962..e2bc35bbd12 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -34,10 +34,15 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.springframework.security.config.Customizer.withDefaults; @@ -95,6 +100,26 @@ public void x509WhenSubjectPrincipalRegexInLambdaThenUsesRegexToExtractPrincipal // @formatter:on } + @Test + public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception { + this.spring.register(UserDetailsServiceBeanConfig.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")); + // @formatter:on + } + + @Test + public void x509WhenUserDetailsServiceAndBeanConfiguredThenDoesNotUseBean() throws Exception { + this.spring.register(UserDetailsServiceAndBeanConfig.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")); + // @formatter:on + } + private T loadCert(String location) { try (InputStream is = new ClassPathResource(location).getInputStream()) { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -206,4 +231,59 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } + @EnableWebSecurity + static class UserDetailsServiceBeanConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + // @formatter:off + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER", "ADMIN") + .build() + ); + // @formatter:on + } + + } + + @EnableWebSecurity + static class UserDetailsServiceAndBeanConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + UserDetailsService customUserDetailsService = new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER", "ADMIN") + .build()); + http + .x509((x509) -> x509 + .userDetailsService(customUserDetailsService) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + // @formatter:off + return mock(UserDetailsService.class); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 1fde85ca402..d591b9b1070 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -368,6 +368,17 @@ public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() thr assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); } + // gh-6802 + @Test + public void oauth2LoginWithOneClientConfiguredAndFormLoginThenRedirectDefaultLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigFormLogin.class); + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + } + // gh-5347 @Test public void oauth2LoginWithOneClientConfiguredAndRequestFaviconNotAuthenticatedThenRedirectDefaultLoginPage() @@ -642,6 +653,26 @@ public void onApplicationEvent(AuthenticationSuccessEvent event) { } + @EnableWebSecurity + static class OAuth2LoginConfigFormLogin extends CommonWebSecurityConfigurerAdapter { + + private final InMemoryClientRegistrationRepository clientRegistrationRepository = new InMemoryClientRegistrationRepository( + GOOGLE_CLIENT_REGISTRATION); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login() + .clientRegistrationRepository(this.clientRegistrationRepository) + .and() + .formLogin(); + // @formatter:on + super.configure(http); + } + + } + @EnableWebSecurity static class OAuth2LoginInLambdaConfig extends CommonLambdaWebSecurityConfigurerAdapter implements ApplicationListener { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index b37040eff36..a7b2a157a8e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -46,6 +46,7 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; @@ -60,6 +61,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.saml2.core.Saml2ErrorCodes; @@ -69,6 +71,7 @@ import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; @@ -80,9 +83,13 @@ import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; @@ -94,6 +101,9 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -104,9 +114,11 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -166,6 +178,19 @@ public void cleanup() { } } + @Test + public void saml2LoginWhenDefaultsThenSaml2AuthenticatedPrincipal() throws Exception { + this.spring.register(Saml2LoginConfig.class, ResourceController.class).autowire(); + // @formatter:off + MockHttpSession session = (MockHttpSession) this.mvc + .perform(post("/login/saml2/sso/registration-id") + .param("SAMLResponse", SIGNED_RESPONSE)) + .andExpect(redirectedUrl("/")).andReturn().getRequest().getSession(false); + this.mvc.perform(get("/").session(session)) + .andExpect(content().string("test@saml.user")); + // @formatter:on + } + @Test public void saml2LoginWhenConfiguringAuthenticationManagerThenTheManagerIsUsed() throws Exception { // setup application context @@ -211,13 +236,45 @@ public void authenticationRequestWhenAuthnRequestContextConverterThenUses() thro assertThat(inflated).contains("ForceAuthn=\"true\""); } + @Test + public void authenticationRequestWhenAuthenticationRequestResolverBeanThenUses() throws Exception { + this.spring.register(CustomAuthenticationRequestResolverBean.class).autowire(); + MvcResult result = this.mvc.perform(get("/saml2/authenticate/registration-id")).andReturn(); + UriComponents components = UriComponentsBuilder.fromHttpUrl(result.getResponse().getRedirectedUrl()).build(); + String samlRequest = components.getQueryParams().getFirst("SAMLRequest"); + String decoded = URLDecoder.decode(samlRequest, "UTF-8"); + String inflated = Saml2Utils.samlInflate(Saml2Utils.samlDecode(decoded)); + assertThat(inflated).contains("ForceAuthn=\"true\""); + } + + @Test + public void authenticationRequestWhenAuthenticationRequestResolverDslThenUses() throws Exception { + this.spring.register(CustomAuthenticationRequestResolverDsl.class).autowire(); + MvcResult result = this.mvc.perform(get("/saml2/authenticate/registration-id")).andReturn(); + UriComponents components = UriComponentsBuilder.fromHttpUrl(result.getResponse().getRedirectedUrl()).build(); + String samlRequest = components.getQueryParams().getFirst("SAMLRequest"); + String decoded = URLDecoder.decode(samlRequest, "UTF-8"); + String inflated = Saml2Utils.samlInflate(Saml2Utils.samlDecode(decoded)); + assertThat(inflated).contains("ForceAuthn=\"true\""); + } + + @Test + public void authenticationRequestWhenAuthenticationRequestResolverAndFactoryThenResolverTakesPrecedence() + throws Exception { + this.spring.register(CustomAuthenticationRequestResolverPrecedence.class).autowire(); + MvcResult result = this.mvc.perform(get("/saml2/authenticate/registration-id")).andReturn(); + UriComponents components = UriComponentsBuilder.fromHttpUrl(result.getResponse().getRedirectedUrl()).build(); + String samlRequest = components.getQueryParams().getFirst("SAMLRequest"); + String decoded = URLDecoder.decode(samlRequest, "UTF-8"); + String inflated = Saml2Utils.samlInflate(Saml2Utils.samlDecode(decoded)); + assertThat(inflated).contains("ForceAuthn=\"true\""); + verifyNoInteractions(this.spring.getContext().getBean(Saml2AuthenticationRequestFactory.class)); + } + @Test public void authenticateWhenCustomAuthenticationConverterThenUses() throws Exception { this.spring.register(CustomAuthenticationConverter.class).autowire(); - RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() - .assertingPartyDetails((party) -> party.verificationX509Credentials( - (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) - .build(); + RelyingPartyRegistration relyingPartyRegistration = this.repository.findByRegistrationId("registration-id"); String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); given(CustomAuthenticationConverter.authenticationConverter.convert(any(HttpServletRequest.class))) .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); @@ -234,10 +291,7 @@ public void authenticateWhenCustomAuthenticationConverterBeanThenUses() throws E this.spring.register(CustomAuthenticationConverterBean.class).autowire(); Saml2AuthenticationTokenConverter authenticationConverter = this.spring.getContext() .getBean(Saml2AuthenticationTokenConverter.class); - RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() - .assertingPartyDetails((party) -> party.verificationX509Credentials( - (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) - .build(); + RelyingPartyRegistration relyingPartyRegistration = this.repository.findByRegistrationId("registration-id"); String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); given(authenticationConverter.convert(any(HttpServletRequest.class))) .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); @@ -303,10 +357,7 @@ public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthe public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate() throws Exception { this.spring.register(CustomLoginProcessingUrlCustomAuthenticationConverter.class).autowire(); - RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() - .assertingPartyDetails((party) -> party.verificationX509Credentials( - (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) - .build(); + RelyingPartyRegistration relyingPartyRegistration = this.repository.findByRegistrationId("registration-id"); String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); given(AUTHENTICATION_CONVERTER.convert(any(HttpServletRequest.class))) .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); @@ -323,10 +374,7 @@ public void authenticateWhenCustomLoginProcessingUrlAndSaml2AuthenticationTokenC this.spring.register(CustomLoginProcessingUrlSaml2AuthenticationTokenConverterBean.class).autowire(); Saml2AuthenticationTokenConverter authenticationConverter = this.spring.getContext() .getBean(Saml2AuthenticationTokenConverter.class); - RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() - .assertingPartyDetails((party) -> party.verificationX509Credentials( - (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) - .build(); + RelyingPartyRegistration relyingPartyRegistration = this.repository.findByRegistrationId("registration-id"); String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); given(authenticationConverter.convert(any(HttpServletRequest.class))) .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); @@ -385,6 +433,21 @@ public boolean supports(Class authentication) { }; } + @EnableWebSecurity + @EnableWebMvc + @Import(Saml2LoginConfigBeans.class) + static class Saml2LoginConfig { + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .saml2Login(Customizer.withDefaults()); + + return http.build(); + } + + } + @EnableWebSecurity @Import(Saml2LoginConfigBeans.class) static class Saml2LoginConfigWithCustomAuthenticationManager extends WebSecurityConfigurerAdapter { @@ -506,6 +569,103 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory() { } + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class CustomAuthenticationRequestResolverBean { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authz) -> authz + .anyRequest().authenticated() + ) + .saml2Login(Customizer.withDefaults()); + // @formatter:on + + return http.build(); + } + + @Bean + Saml2AuthenticationRequestResolver authenticationRequestResolver( + RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( + registrations); + OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver( + registrationResolver); + delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); + return delegate; + } + + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class CustomAuthenticationRequestResolverDsl { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http, RelyingPartyRegistrationRepository registrations) + throws Exception { + // @formatter:off + http + .authorizeRequests((authz) -> authz + .anyRequest().authenticated() + ) + .saml2Login((saml2) -> saml2 + .authenticationRequestResolver(authenticationRequestResolver(registrations)) + ); + // @formatter:on + + return http.build(); + } + + Saml2AuthenticationRequestResolver authenticationRequestResolver( + RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( + registrations); + OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver( + registrationResolver); + delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); + return delegate; + } + + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class CustomAuthenticationRequestResolverPrecedence { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authz) -> authz + .anyRequest().authenticated() + ) + .saml2Login(Customizer.withDefaults()); + // @formatter:on + + return http.build(); + } + + @Bean + Saml2AuthenticationRequestFactory authenticationRequestFactory() { + return mock(Saml2AuthenticationRequestFactory.class); + } + + @Bean + Saml2AuthenticationRequestResolver authenticationRequestResolver( + RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( + registrations); + OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver( + registrationResolver); + delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); + return delegate; + } + + } + @EnableWebSecurity @Import(Saml2LoginConfigBeans.class) static class CustomAuthenticationConverter extends WebSecurityConfigurerAdapter { @@ -630,12 +790,26 @@ SecurityContextRepository securityContextRepository() { @Bean RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() + .signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartySigningCredential())) + .assertingPartyDetails((party) -> party.verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); - given(repository.findByRegistrationId(anyString())) - .willReturn(TestRelyingPartyRegistrations.relyingPartyRegistration().build()); + given(repository.findByRegistrationId(anyString())).willReturn(registration); return repository; } } + @RestController + static class ResourceController { + + @GetMapping("/") + String user(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) { + return principal.getName(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java index 051327548ae..33a22073861 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -20,7 +20,11 @@ import java.time.Instant; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -72,6 +76,9 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -252,12 +259,38 @@ public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() th DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", Collections.emptyMap()); principal.setRelyingPartyRegistrationId("get"); + Saml2Authentication user = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + MvcResult result = this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .param("Signature", this.apLogoutRequestSignature).with(samlQueryString()).with(authentication(user))) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/response"); + verify(getBean(LogoutHandler.class)).logout(any(), any(), any()); + } + + // gh-11235 + @Test + public void saml2LogoutRequestWhenLowercaseEncodingThenLogsOutAndSendsLogoutResponse() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + String apLogoutRequest = "nZFNa4QwEIb/iuQeP6K7dYO6FKQg2B622x56G3WwgiY2E8v239fqCksPPfSWIXmfNw+THC9D73yi\r\n" + + "oU6rlAWuzxxUtW461abs5fzAY3bMEoKhF6Msdasne8KPCck6c1KRXK9SNhklNVBHUsGAJG0tn+8f\r\n" + + "SylcX45GW13rnjn5HOwU2KXt3dqRpOeZ0cULDGOPrjat1y8t3gL2zFrGnCJPWXkKcR8KCHY8xmrP\r\n" + + "Iz868OpOVLwO4wohggagmd8STVgosqBsyoQvBPd3XITnIJaRL8PYjcThjTmvm/f8SXa1lEvY3Nr9\r\n" + + "LQdEaH6EWAYjR2U7+8W7JvFucRv8aY4X+b/g03zaoCsmu46/FpN9Aw=="; + String apLogoutRequestRelayState = "d118dbd5-3853-4268-b3e5-c40fc033fa2f"; + String apLogoutRequestSignature = "VZ7rWa5u3hIX60fAQs/gBQZWDP2BAIlCMMrNrTHafoKKj0uXWnuITYLuL8NdsWmyQN0+fqWW4X05+BqiLpL80jHLmQR5RVqqL1EtVv1SpPUna938lgz2sOliuYmfQNj4Bmd+Z5G1K6QhbVrtfb7TQHURjUafzfRm8+jGz3dPjVBrn/rD/umfGoSn6RuWngugcMNL4U0A+JcEh1NSfSYNVz7y+MqlW1UhX2kF86rm97ERCrxay7Gh/bI2f3fJPJ1r+EyLjzrDUkqw5cva3rVlFgEQouMVu35lUJn7SFompW8oTxkI23oc/t+AGZqaBupNITNdjyGCBpfukZ69EZrj8g=="; + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("get"); Saml2Authentication user = new Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER")); MvcResult result = this.mvc - .perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) - .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) - .param("Signature", this.apLogoutRequestSignature).with(authentication(user))) + .perform(get("/logout/saml2/slo").param("SAMLRequest", apLogoutRequest) + .param("RelayState", apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .param("Signature", apLogoutRequestSignature) + .with(new SamlQueryStringRequestPostProcessor(true)).with(authentication(user))) .andExpect(status().isFound()).andReturn(); String location = result.getResponse().getHeader("Location"); assertThat(location).startsWith("https://ap.example.org/logout/saml2/response"); @@ -316,8 +349,8 @@ public void saml2LogoutResponseWhenDefaultsThenRedirects() throws Exception { assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNotNull(); this.mvc.perform(get("/logout/saml2/slo").session(((MockHttpSession) this.request.getSession())) .param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState) - .param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature)) - .andExpect(status().isFound()).andExpect(redirectedUrl("/login?logout")); + .param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature) + .with(samlQueryString())).andExpect(status().isFound()).andExpect(redirectedUrl("/login?logout")); verifyNoInteractions(getBean(LogoutHandler.class)); assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull(); } @@ -334,8 +367,9 @@ public void saml2LogoutResponseWhenInvalidSamlResponseThen401() throws Exception Saml2Utils.samlInflate(Saml2Utils.samlDecode(this.apLogoutResponse)).getBytes(StandardCharsets.UTF_8)); this.mvc.perform(post("/logout/saml2/slo").session((MockHttpSession) this.request.getSession()) .param("SAMLResponse", deflatedApLogoutResponse).param("RelayState", this.rpLogoutRequestRelayState) - .param("SigAlg", this.apLogoutRequestSigAlg).param("Signature", this.apLogoutResponseSignature)) - .andExpect(status().reason(containsString("invalid_signature"))).andExpect(status().isUnauthorized()); + .param("SigAlg", this.apLogoutRequestSigAlg).param("Signature", this.apLogoutResponseSignature) + .with(samlQueryString())).andExpect(status().reason(containsString("invalid_signature"))) + .andExpect(status().isUnauthorized()); verifyNoInteractions(getBean(LogoutHandler.class)); } @@ -398,6 +432,10 @@ private T getBean(Class clazz) { return this.spring.getContext().getBean(clazz); } + private SamlQueryStringRequestPostProcessor samlQueryString() { + return new SamlQueryStringRequestPostProcessor(); + } + @EnableWebSecurity @Import(Saml2LoginConfigBeans.class) static class Saml2LogoutDefaultsConfig { @@ -602,4 +640,40 @@ public O postProcess(O object) { } + static class SamlQueryStringRequestPostProcessor implements RequestPostProcessor { + + private Function urlEncodingPostProcessor = Function.identity(); + + SamlQueryStringRequestPostProcessor() { + this(false); + } + + SamlQueryStringRequestPostProcessor(boolean lowercased) { + if (lowercased) { + Pattern encoding = Pattern.compile("%\\d[A-Fa-f]"); + this.urlEncodingPostProcessor = (encoded) -> { + Matcher m = encoding.matcher(encoded); + while (m.find()) { + String found = m.group(0); + encoded = encoded.replace(found, found.toLowerCase()); + } + return encoded; + }; + } + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry entries : request.getParameterMap().entrySet()) { + builder.queryParam(entries.getKey(), + UriUtils.encode(entries.getValue()[0], StandardCharsets.ISO_8859_1)); + } + String queryString = this.urlEncodingPostProcessor.apply(builder.build(true).toUriString().substring(1)); + request.setQueryString(queryString); + return request; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelectorTest.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelectorTest.java new file mode 100644 index 00000000000..1ba668d3312 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelectorTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link ReactiveOAuth2ClientImportSelector}. + * + * @author Alavudin Kuttikkattil + */ +@ExtendWith(SpringTestContextExtension.class) +public class ReactiveOAuth2ClientImportSelectorTest { + + public final SpringTestContext spring = new SpringTestContext(this); + + WebTestClient client; + + @Autowired + public void setApplicationContext(ApplicationContext context) { + // @formatter:off + this.client = WebTestClient + .bindToApplicationContext(context) + .build(); + // @formatter:on + } + + @Test + public void requestWhenAuthorizedClientManagerConfiguredThenUsed() { + String clientRegistrationId = "client"; + String principalName = "user"; + ReactiveClientRegistrationRepository clientRegistrationRepository = mock( + ReactiveClientRegistrationRepository.class); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = mock( + ServerOAuth2AuthorizedClientRepository.class); + ReactiveOAuth2AuthorizedClientManager authorizedClientManager = mock( + ReactiveOAuth2AuthorizedClientManager.class); + ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials() + .registrationId(clientRegistrationId).build(); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principalName, + TestOAuth2AccessTokens.noScopes()); + given(authorizedClientManager.authorize(any())).willReturn(Mono.just(authorizedClient)); + OAuth2AuthorizedClientManagerRegisteredConfig.CLIENT_REGISTRATION_REPOSITORY = clientRegistrationRepository; + OAuth2AuthorizedClientManagerRegisteredConfig.AUTHORIZED_CLIENT_REPOSITORY = authorizedClientRepository; + OAuth2AuthorizedClientManagerRegisteredConfig.AUTHORIZED_CLIENT_MANAGER = authorizedClientManager; + this.spring.register(OAuth2AuthorizedClientManagerRegisteredConfig.class).autowire(); + // @formatter:off + this.client + .get() + .uri("http://localhost/authorized-client") + .headers((headers) -> headers.setBasicAuth("user", "password")).exchange().expectStatus().isOk() + .expectBody(String.class).isEqualTo("resolved"); + // @formatter:on + verify(authorizedClientManager).authorize(any()); + verifyNoInteractions(clientRegistrationRepository); + verifyNoInteractions(authorizedClientRepository); + } + + @Test + public void requestWhenAuthorizedClientManagerNotConfigureThenUseDefaultAuthorizedClientManager() { + String clientRegistrationId = "client"; + String principalName = "user"; + ReactiveClientRegistrationRepository clientRegistrationRepository = mock( + ReactiveClientRegistrationRepository.class); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = mock( + ServerOAuth2AuthorizedClientRepository.class); + ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials() + .registrationId(clientRegistrationId).build(); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principalName, + TestOAuth2AccessTokens.noScopes()); + OAuth2AuthorizedClientManagerRegisteredConfig.CLIENT_REGISTRATION_REPOSITORY = clientRegistrationRepository; + OAuth2AuthorizedClientManagerRegisteredConfig.AUTHORIZED_CLIENT_REPOSITORY = authorizedClientRepository; + OAuth2AuthorizedClientManagerRegisteredConfig.AUTHORIZED_CLIENT_MANAGER = null; + given(authorizedClientRepository.loadAuthorizedClient(any(), any(), any())) + .willReturn(Mono.just(authorizedClient)); + this.spring.register(OAuth2AuthorizedClientManagerRegisteredConfig.class).autowire(); + // @formatter:off + this.client + .get() + .uri("http://localhost/authorized-client") + .headers((headers) -> headers.setBasicAuth("user", "password")).exchange().expectStatus().isOk() + .expectBody(String.class).isEqualTo("resolved"); + // @formatter:on + } + + @EnableWebFlux + @EnableWebFluxSecurity + static class OAuth2AuthorizedClientManagerRegisteredConfig { + + static ReactiveClientRegistrationRepository CLIENT_REGISTRATION_REPOSITORY; + static ServerOAuth2AuthorizedClientRepository AUTHORIZED_CLIENT_REPOSITORY; + static ReactiveOAuth2AuthorizedClientManager AUTHORIZED_CLIENT_MANAGER; + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + return http.build(); + } + + @Bean + ReactiveClientRegistrationRepository clientRegistrationRepository() { + return CLIENT_REGISTRATION_REPOSITORY; + } + + @Bean + ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { + return AUTHORIZED_CLIENT_REPOSITORY; + } + + @Bean + ReactiveOAuth2AuthorizedClientManager authorizedClientManager() { + return AUTHORIZED_CLIENT_MANAGER; + } + + @RestController + class Controller { + + @GetMapping("/authorized-client") + String authorizedClient( + @RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { + return (authorizedClient != null) ? "resolved" : "not-resolved"; + } + + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java new file mode 100644 index 00000000000..54372a3a569 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import java.util.HashMap; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.mock.web.MockServletConfig; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.stereotype.Controller; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class WebSocketMessageBrokerSecurityConfigurationDocTests { + + AnnotationConfigWebApplicationContext context; + + TestingAuthenticationToken messageUser; + + CsrfToken token; + + String sessionAttr; + + @BeforeEach + public void setup() { + this.token = new DefaultCsrfToken("header", "param", "token"); + this.sessionAttr = "sessionAttr"; + this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER"); + } + + @AfterEach + public void cleanup() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void securityMappings() { + loadConfig(WebSocketSecurityConfig.class); + clientInboundChannel().send(message("/user/queue/errors", SimpMessageType.SUBSCRIBE)); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> clientInboundChannel().send(message("/denyAll", SimpMessageType.MESSAGE))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + private void loadConfig(Class... configs) { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(configs); + this.context.register(WebSocketConfig.class, SyncExecutorConfig.class); + this.context.setServletConfig(new MockServletConfig()); + this.context.refresh(); + } + + private MessageChannel clientInboundChannel() { + return this.context.getBean("clientInboundChannel", MessageChannel.class); + } + + private Message message(String destination, SimpMessageType type) { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type); + return message(headers, destination); + } + + private Message message(SimpMessageHeaderAccessor headers, String destination) { + headers.setSessionId("123"); + headers.setSessionAttributes(new HashMap<>()); + if (destination != null) { + headers.setDestination(destination); + } + if (this.messageUser != null) { + headers.setUser(this.messageUser); + } + return new GenericMessage<>("hi", headers.getMessageHeaders()); + } + + @Controller + static class MyController { + + @MessageMapping("/authentication") + void authentication(@AuthenticationPrincipal String un) { + // ... do something ... + } + + } + + @Configuration + @EnableWebSocketSecurity + static class WebSocketSecurityConfig { + + @Bean + AuthorizationManager> authorizationManager( + MessageMatcherDelegatingAuthorizationManager.Builder messages) { + messages.nullDestMatcher().authenticated() + // <1> + .simpSubscribeDestMatchers("/user/queue/errors").permitAll() + // <2> + .simpDestMatchers("/app/**").hasRole("USER") + // <3> + .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4> + .simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll() // <5> + .anyMessage().denyAll(); // <6> + return messages.build(); + } + + } + + @Configuration + @EnableWebSocketMessageBroker + static class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/chat").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll"); + } + + @Bean + MyController myController() { + return new MyController(); + } + + } + + @Configuration + static class SyncExecutorConfig { + + @Bean + static SyncExecutorSubscribableChannelPostProcessor postProcessor() { + return new SyncExecutorSubscribableChannelPostProcessor(); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java new file mode 100644 index 00000000000..97c6f2eb5f3 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java @@ -0,0 +1,786 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.MethodParameter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.support.AbstractMessageChannel; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletConfig; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; +import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; +import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; +import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.security.web.csrf.MissingCsrfTokenException; +import org.springframework.stereotype.Controller; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.server.HandshakeFailureException; +import org.springframework.web.socket.server.HandshakeHandler; +import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; +import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler; +import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; + +public class WebSocketMessageBrokerSecurityConfigurationTests { + + AnnotationConfigWebApplicationContext context; + + TestingAuthenticationToken messageUser; + + CsrfToken token; + + String sessionAttr; + + @BeforeEach + public void setup() { + this.token = new DefaultCsrfToken("header", "param", "token"); + this.sessionAttr = "sessionAttr"; + this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER"); + } + + @AfterEach + public void cleanup() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void simpleRegistryMappings() { + loadConfig(SockJsSecurityConfig.class); + clientInboundChannel().send(message("/permitAll")); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> clientInboundChannel().send(message("/denyAll"))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void annonymousSupported() { + loadConfig(SockJsSecurityConfig.class); + this.messageUser = null; + clientInboundChannel().send(message("/permitAll")); + } + + // gh-3797 + @Test + public void beanResolver() { + loadConfig(SockJsSecurityConfig.class); + this.messageUser = null; + clientInboundChannel().send(message("/beanResolver")); + } + + @Test + public void addsAuthenticationPrincipalResolver() { + loadConfig(SockJsSecurityConfig.class); + MessageChannel messageChannel = clientInboundChannel(); + Message message = message("/permitAll/authentication"); + messageChannel.send(message); + assertThat(this.context.getBean(MyController.class).authenticationPrincipal) + .isEqualTo((String) this.messageUser.getPrincipal()); + } + + @Test + public void addsAuthenticationPrincipalResolverWhenNoAuthorization() { + loadConfig(NoInboundSecurityConfig.class); + MessageChannel messageChannel = clientInboundChannel(); + Message message = message("/permitAll/authentication"); + messageChannel.send(message); + assertThat(this.context.getBean(MyController.class).authenticationPrincipal) + .isEqualTo((String) this.messageUser.getPrincipal()); + } + + @Test + public void addsCsrfProtectionWhenNoAuthorization() { + loadConfig(NoInboundSecurityConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + Message message = message(headers, "/authentication"); + MessageChannel messageChannel = clientInboundChannel(); + assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message)) + .withCauseInstanceOf(MissingCsrfTokenException.class); + } + + @Test + public void csrfProtectionForConnect() { + loadConfig(SockJsSecurityConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + Message message = message(headers, "/authentication"); + MessageChannel messageChannel = clientInboundChannel(); + assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message)) + .withCauseInstanceOf(MissingCsrfTokenException.class); + } + + @Test + @Disabled // to be added back in with the introduction of DSL support + public void csrfProtectionDisabledForConnect() { + loadConfig(CsrfDisabledSockJsSecurityConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + Message message = message(headers, "/permitAll/connect"); + MessageChannel messageChannel = clientInboundChannel(); + messageChannel.send(message); + } + + @Test + public void csrfProtectionDefinedByBean() { + loadConfig(SockJsProxylessSecurityConfig.class); + MessageChannel messageChannel = clientInboundChannel(); + Stream> interceptors = ((AbstractMessageChannel) messageChannel) + .getInterceptors().stream().map(ChannelInterceptor::getClass); + assertThat(interceptors).contains(CsrfChannelInterceptor.class); + } + + @Test + public void messagesConnectUseCsrfTokenHandshakeInterceptor() throws Exception { + loadConfig(SockJsSecurityConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + Message message = message(headers, "/authentication"); + MockHttpServletRequest request = sockjsHttpRequest("/chat"); + HttpRequestHandler handler = handler(request); + handler.handleRequest(request, new MockHttpServletResponse()); + assertHandshake(request); + } + + @Test + public void messagesConnectUseCsrfTokenHandshakeInterceptorMultipleMappings() throws Exception { + loadConfig(SockJsSecurityConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + Message message = message(headers, "/authentication"); + MockHttpServletRequest request = sockjsHttpRequest("/other"); + HttpRequestHandler handler = handler(request); + handler.handleRequest(request, new MockHttpServletResponse()); + assertHandshake(request); + } + + @Test + public void messagesConnectWebSocketUseCsrfTokenHandshakeInterceptor() throws Exception { + loadConfig(WebSocketSecurityConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + Message message = message(headers, "/authentication"); + MockHttpServletRequest request = websocketHttpRequest("/websocket"); + HttpRequestHandler handler = handler(request); + handler.handleRequest(request, new MockHttpServletResponse()); + assertHandshake(request); + } + + @Test + public void msmsRegistryCustomPatternMatcher() { + loadConfig(MsmsRegistryCustomPatternMatcherConfig.class); + clientInboundChannel().send(message("/app/a.b")); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> clientInboundChannel().send(message("/app/a.b.c"))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void overrideMsmsRegistryCustomPatternMatcher() { + loadConfig(OverrideMsmsRegistryCustomPatternMatcherConfig.class); + clientInboundChannel().send(message("/app/a/b")); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c"))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void defaultPatternMatcher() { + loadConfig(DefaultPatternMatcherConfig.class); + clientInboundChannel().send(message("/app/a/b")); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c"))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void customExpression() { + loadConfig(CustomExpressionConfig.class); + clientInboundChannel().send(message("/denyRob")); + this.messageUser = new TestingAuthenticationToken("rob", "password", "ROLE_USER"); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> clientInboundChannel().send(message("/denyRob"))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void channelSecurityInterceptorUsesMetadataSourceBeanWhenProxyingDisabled() { + loadConfig(SockJsProxylessSecurityConfig.class); + AbstractMessageChannel messageChannel = clientInboundChannel(); + AuthorizationManager> authorizationManager = this.context.getBean(AuthorizationManager.class); + for (ChannelInterceptor interceptor : messageChannel.getInterceptors()) { + if (interceptor instanceof AuthorizationChannelInterceptor) { + assertThat(ReflectionTestUtils.getField(interceptor, "preSendAuthorizationManager")) + .isSameAs(authorizationManager); + return; + } + } + fail("did not find AuthorizationChannelInterceptor"); + } + + @Test + public void securityContextChannelInterceptorDefinedByBean() { + loadConfig(SockJsProxylessSecurityConfig.class); + MessageChannel messageChannel = clientInboundChannel(); + Stream> interceptors = ((AbstractMessageChannel) messageChannel) + .getInterceptors().stream().map(ChannelInterceptor::getClass); + assertThat(interceptors).contains(SecurityContextChannelInterceptor.class); + } + + @Test + public void inboundChannelSecurityDefinedByBean() { + loadConfig(SockJsProxylessSecurityConfig.class); + MessageChannel messageChannel = clientInboundChannel(); + Stream> interceptors = ((AbstractMessageChannel) messageChannel) + .getInterceptors().stream().map(ChannelInterceptor::getClass); + assertThat(interceptors).contains(AuthorizationChannelInterceptor.class); + } + + private void assertHandshake(HttpServletRequest request) { + TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); + assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token); + assertThat(handshakeHandler.attributes.get(this.sessionAttr)) + .isEqualTo(request.getSession().getAttribute(this.sessionAttr)); + } + + private HttpRequestHandler handler(HttpServletRequest request) throws Exception { + HandlerMapping handlerMapping = this.context.getBean(HandlerMapping.class); + return (HttpRequestHandler) handlerMapping.getHandler(request).getHandler(); + } + + private MockHttpServletRequest websocketHttpRequest(String mapping) { + MockHttpServletRequest request = sockjsHttpRequest(mapping); + request.setRequestURI(mapping); + return request; + } + + private MockHttpServletRequest sockjsHttpRequest(String mapping) { + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + request.setMethod("GET"); + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket"); + request.setRequestURI(mapping + "/289/tpyx6mde/websocket"); + request.getSession().setAttribute(this.sessionAttr, "sessionValue"); + request.setAttribute(CsrfToken.class.getName(), this.token); + return request; + } + + private Message message(String destination) { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); + return message(headers, destination); + } + + private Message message(SimpMessageHeaderAccessor headers, String destination) { + headers.setSessionId("123"); + headers.setSessionAttributes(new HashMap<>()); + if (destination != null) { + headers.setDestination(destination); + } + if (this.messageUser != null) { + headers.setUser(this.messageUser); + } + return new GenericMessage<>("hi", headers.getMessageHeaders()); + } + + private T clientInboundChannel() { + return (T) this.context.getBean("clientInboundChannel", MessageChannel.class); + } + + private void loadConfig(Class... configs) { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(configs); + this.context.setServletConfig(new MockServletConfig()); + this.context.refresh(); + } + + @Configuration + @EnableWebSocketMessageBroker + @EnableWebSocketSecurity + @Import(SyncExecutorConfig.class) + static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { + + // @formatter:off + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + .addEndpoint("/other") + .setHandshakeHandler(testHandshakeHandler()); + } + // @formatter:on + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setPathMatcher(new AntPathMatcher(".")); + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/app"); + } + + // @formatter:off + @Bean + AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { + messages + .simpDestMatchers("/app/a.*").permitAll() + .anyMessage().denyAll(); + + return messages.build(); + } + // @formatter:on + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Configuration + @EnableWebSocketMessageBroker + @EnableWebSocketSecurity + @Import(SyncExecutorConfig.class) + static class OverrideMsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { + + // @formatter:off + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + .addEndpoint("/other") + .setHandshakeHandler(testHandshakeHandler()); + } + // @formatter:on + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setPathMatcher(new AntPathMatcher(".")); + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/app"); + } + + // @formatter:off + @Bean + AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { + messages + .simpDestPathMatcher(new AntPathMatcher()) + .simpDestMatchers("/app/a/*").permitAll() + .anyMessage().denyAll(); + return messages.build(); + } + // @formatter:on + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Configuration + @EnableWebSocketMessageBroker + @EnableWebSocketSecurity + @Import(SyncExecutorConfig.class) + static class DefaultPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { + + // @formatter:off + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + .addEndpoint("/other") + .setHandshakeHandler(testHandshakeHandler()); + } + // @formatter:on + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/app"); + } + + // @formatter:off + @Bean + AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { + messages + .simpDestMatchers("/app/a/*").permitAll() + .anyMessage().denyAll(); + + return messages.build(); + } + // @formatter:on + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Configuration + @EnableWebSocketMessageBroker + @EnableWebSocketSecurity + @Import(SyncExecutorConfig.class) + static class CustomExpressionConfig implements WebSocketMessageBrokerConfigurer { + + // @formatter:off + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + .addEndpoint("/other") + .setHandshakeHandler(testHandshakeHandler()); + } + // @formatter:on + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/app"); + } + + @Bean + AuthorizationManager> authorizationManager() { + return (authentication, message) -> { + Authentication auth = authentication.get(); + return new AuthorizationDecision(auth != null && !"rob".equals(auth.getName())); + }; + } + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Controller + static class MyController { + + String authenticationPrincipal; + + MyCustomArgument myCustomArgument; + + @MessageMapping("/authentication") + void authentication(@AuthenticationPrincipal String un) { + this.authenticationPrincipal = un; + } + + @MessageMapping("/myCustom") + void myCustom(MyCustomArgument myCustomArgument) { + this.myCustomArgument = myCustomArgument; + } + + } + + static class MyCustomArgument { + + MyCustomArgument(String notDefaultConstr) { + } + + } + + static class MyCustomArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().isAssignableFrom(MyCustomArgument.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + return new MyCustomArgument(""); + } + + } + + static class TestHandshakeHandler implements HandshakeHandler { + + Map attributes; + + @Override + public boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Map attributes) throws HandshakeFailureException { + this.attributes = attributes; + if (wsHandler instanceof SockJsWebSocketHandler) { + // work around SPR-12716 + SockJsWebSocketHandler sockJs = (SockJsWebSocketHandler) wsHandler; + WebSocketServerSockJsSession session = (WebSocketServerSockJsSession) ReflectionTestUtils + .getField(sockJs, "sockJsSession"); + this.attributes = session.getAttributes(); + } + return true; + } + + } + + @Configuration + @EnableWebSocketSecurity + @EnableWebSocketMessageBroker + @Import(SyncExecutorConfig.class) + static class SockJsSecurityConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // @formatter:off + registry.addEndpoint("/other").setHandshakeHandler(testHandshakeHandler()) + .withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); + registry.addEndpoint("/chat").setHandshakeHandler(testHandshakeHandler()) + .withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); + // @formatter:on + } + + // @formatter:off + @Bean + AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages, + SecurityCheck security) { + AuthorizationManager> beanResolver = + (authentication, context) -> new AuthorizationDecision(security.check()); + messages + .simpDestMatchers("/permitAll/**").permitAll() + .simpDestMatchers("/beanResolver/**").access(beanResolver) + .anyMessage().denyAll(); + return messages.build(); + } + // @formatter:on + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll"); + } + + @Bean + MyController myController() { + return new MyController(); + } + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + @Bean + SecurityCheck security() { + return new SecurityCheck(); + } + + static class SecurityCheck { + + private boolean check; + + boolean check() { + this.check = !this.check; + return this.check; + } + + } + + } + + @Configuration + @EnableWebSocketSecurity + @EnableWebSocketMessageBroker + @Import(SyncExecutorConfig.class) + static class NoInboundSecurityConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // @formatter:off + registry.addEndpoint("/other") + .withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); + registry.addEndpoint("/chat") + .withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); + // @formatter:on + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/"); + registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll"); + } + + @Bean + MyController myController() { + return new MyController(); + } + + } + + @Configuration + @Import(SockJsSecurityConfig.class) + static class CsrfDisabledSockJsSecurityConfig { + + @Bean + Consumer> channelInterceptorCustomizer() { + return (interceptors) -> interceptors.remove(1); + } + + } + + @Configuration + @EnableWebSocketSecurity + @EnableWebSocketMessageBroker + @Import(SyncExecutorConfig.class) + static class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // @formatter:off + registry.addEndpoint("/websocket") + .setHandshakeHandler(testHandshakeHandler()) + .addInterceptors(new HttpSessionHandshakeInterceptor()); + // @formatter:on + } + + @Bean + AuthorizationManager> authorizationManager( + MessageMatcherDelegatingAuthorizationManager.Builder messages) { + // @formatter:off + messages + .simpDestMatchers("/permitAll/**").permitAll() + .anyMessage().denyAll(); + // @formatter:on + return messages.build(); + } + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Configuration + @EnableWebSocketSecurity + @EnableWebSocketMessageBroker + @Import(SyncExecutorConfig.class) + static class UsingLegacyConfigurerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // @formatter:off + registry.addEndpoint("/websocket") + .setHandshakeHandler(testHandshakeHandler()) + .addInterceptors(new HttpSessionHandshakeInterceptor()); + // @formatter:on + } + + @Override + public void configureInbound(MessageSecurityMetadataSourceRegistry messages) { + // @formatter:off + messages + .simpDestMatchers("/permitAll/**").permitAll() + .anyMessage().denyAll(); + // @formatter:on + } + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSocketSecurity + @EnableWebSocketMessageBroker + @Import(SyncExecutorConfig.class) + static class SockJsProxylessSecurityConfig implements WebSocketMessageBrokerConfigurer { + + private ApplicationContext context; + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // @formatter:off + registry.addEndpoint("/chat") + .setHandshakeHandler(this.context.getBean(TestHandshakeHandler.class)) + .withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); + // @formatter:on + } + + @Autowired + void setContext(ApplicationContext context) { + this.context = context; + } + + // @formatter:off + @Bean + AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { + messages + .anyMessage().denyAll(); + return messages.build(); + } + // @formatter:on + + @Bean + TestHandshakeHandler testHandshakeHandler() { + return new TestHandshakeHandler(); + } + + } + + @Configuration + static class SyncExecutorConfig { + + @Bean + static SyncExecutorSubscribableChannelPostProcessor postProcessor() { + return new SyncExecutorSubscribableChannelPostProcessor(); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationConfigurationGh3935Tests.java b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationConfigurationGh3935Tests.java index 6d5c0d50d75..5f71fabd981 100644 --- a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationConfigurationGh3935Tests.java +++ b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationConfigurationGh3935Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -72,7 +72,7 @@ public void delegateUsesExisitingAuthentication() { AuthenticationManager authenticationManager = this.adapter.authenticationManager; assertThat(authenticationManager).isNotNull(); Authentication auth = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken(username, password)); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated(username, password)); verify(this.uds).loadUserByUsername(username); assertThat(auth.getPrincipal()).isEqualTo(PasswordEncodedUser.user()); } diff --git a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java index df39c4b8b32..ebdb8ee9ddd 100644 --- a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.ProviderManager; @@ -33,6 +34,7 @@ import org.springframework.security.authentication.event.AbstractAuthenticationEvent; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.util.InMemoryXmlWebApplicationContext; import org.springframework.security.util.FieldUtils; import org.springframework.test.web.servlet.MockMvc; @@ -89,6 +91,16 @@ public void onlyOneEventPublisherIsRegisteredForMultipleAuthenticationManagers() assertThat(context.getBeansOfType(AuthenticationEventPublisher.class)).hasSize(1); } + @Test + // gh-8767 + public void multipleAuthenticationManagersAndDisableBeanDefinitionOverridingThenNoException() { + InMemoryXmlWebApplicationContext xmlContext = new InMemoryXmlWebApplicationContext( + CONTEXT + '\n' + CONTEXT_MULTI); + xmlContext.setAllowBeanDefinitionOverriding(false); + ConfigurableApplicationContext context = this.spring.context(xmlContext).getContext(); + assertThat(context.getBeansOfType(AuthenticationManager.class)).hasSize(2); + } + @Test public void eventsArePublishedByDefault() throws Exception { ConfigurableApplicationContext appContext = this.spring.context(CONTEXT).getContext(); @@ -98,7 +110,7 @@ public void eventsArePublishedByDefault() throws Exception { Object eventPublisher = FieldUtils.getFieldValue(pm, "eventPublisher"); assertThat(eventPublisher).isNotNull(); assertThat(eventPublisher instanceof DefaultAuthenticationEventPublisher).isTrue(); - pm.authenticate(new UsernamePasswordAuthenticationToken("bob", "bobspassword")); + pm.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword")); assertThat(listener.events).hasSize(1); } diff --git a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParserTests.java index b32069216dc..eccb380c6b4 100644 --- a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -42,7 +42,8 @@ public class AuthenticationProviderBeanDefinitionParserTests { private AbstractXmlApplicationContext appContext; - private UsernamePasswordAuthenticationToken bob = new UsernamePasswordAuthenticationToken("bob", "bobspassword"); + private UsernamePasswordAuthenticationToken bob = UsernamePasswordAuthenticationToken.unauthenticated("bob", + "bobspassword"); @AfterEach public void closeAppContext() { diff --git a/config/src/test/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParserTests.java index 75ae43bebfe..6cd758bbc2d 100644 --- a/config/src/test/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -129,7 +129,7 @@ public void isSupportedByAuthenticationProviderElement() { + DATA_SOURCE); // @formatter:on AuthenticationManager mgr = (AuthenticationManager) this.appContext.getBean(BeanIds.AUTHENTICATION_MANAGER); - mgr.authenticate(new UsernamePasswordAuthenticationToken("rod", "koala")); + mgr.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala")); } @Test @@ -146,7 +146,7 @@ public void cacheIsInjectedIntoAuthenticationProvider() { ProviderManager mgr = (ProviderManager) this.appContext.getBean(BeanIds.AUTHENTICATION_MANAGER); DaoAuthenticationProvider provider = (DaoAuthenticationProvider) mgr.getProviders().get(0); assertThat(this.appContext.getBean("userCache")).isSameAs(provider.getUserCache()); - provider.authenticate(new UsernamePasswordAuthenticationToken("rod", "koala")); + provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala")); assertThat(provider.getUserCache().getUserFromCache("rod")).isNotNull() .withFailMessage("Cache should contain user after authentication"); } diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index 35d857c84a9..cd86ba4fb4d 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -16,9 +16,10 @@ package org.springframework.security.config.doc; +import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -60,11 +61,11 @@ public class XsdDocumentedTests { "nsa-frame-options-from-parameter"); // @formatter:on - String referenceLocation = "../docs/modules/ROOT/pages/servlet/appendix/namespace.adoc"; + String referenceLocation = "../docs/modules/ROOT/pages/servlet/appendix/namespace"; String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.6.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.8.xsd"; XmlSupport xml = new XmlSupport(); @@ -149,8 +150,8 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() throws I .getParentFile() .list((dir, name) -> name.endsWith(".xsd")); // @formatter:on - assertThat(schemas.length).isEqualTo(18) - .withFailMessage("the count is equal to 18, if not then schemaDocument needs updating"); + assertThat(schemas.length).isEqualTo(20) + .withFailMessage("the count is equal to 20, if not then schemaDocument needs updating"); } /** @@ -163,7 +164,7 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() throws I public void countReferencesWhenReviewingDocumentationThenEntireSchemaIsIncluded() throws IOException { Map elementsByElementName = this.xml.elementsByElementName(this.schemaDocumentLocation); // @formatter:off - List documentIds = Files.lines(Paths.get(this.referenceLocation)) + List documentIds = namespaceLines() .filter((line) -> line.matches("\\[\\[(nsa-.*)\\]\\]")) .map((line) -> line.substring(2, line.length() - 2)) .collect(Collectors.toList()); @@ -189,7 +190,7 @@ public void countLinksWhenReviewingDocumentationThenParentsAndChildrenAreCorrect Map> docAttrNameToParents = new TreeMap<>(); String docAttrName = null; Map> currentDocAttrNameToElmt = null; - List lines = Files.readAllLines(Paths.get(this.referenceLocation)); + List lines = namespaceLines().collect(Collectors.toList()); for (String line : lines) { if (line.matches("^\\[\\[.*\\]\\]$")) { String id = line.substring(2, line.length() - 2); @@ -212,6 +213,13 @@ else if (id.endsWith("-attributes") || docAttrName != null && !id.startsWith(doc String elmtId = line.replaceAll(expression, "$1"); currentDocAttrNameToElmt.computeIfAbsent(docAttrName, (key) -> new ArrayList<>()).add(elmtId); } + else { + expression = ".*xref:.*#(nsa-.*)\\[.*\\]"; + if (line.matches(expression)) { + String elmtId = line.replaceAll(expression, "$1"); + currentDocAttrNameToElmt.computeIfAbsent(docAttrName, (key) -> new ArrayList<>()).add(elmtId); + } + } } } Map elementNameToElement = this.xml.elementsByElementName(this.schemaDocumentLocation); @@ -295,4 +303,17 @@ public void countWhenReviewingDocumentationThenAllElementsDocumented() throws IO assertThat(notDocAttrIds).isEmpty(); } + private Stream namespaceLines() { + return Stream.of(new File(this.referenceLocation).listFiles()).map(File::toPath).flatMap(this::fileLines); + } + + private Stream fileLines(Path path) { + try { + return Files.lines(path); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java index 990a6482211..c0fa5f46b35 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java @@ -16,6 +16,7 @@ package org.springframework.security.config.http; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; @@ -26,12 +27,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.web.FilterChainProxy; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -60,6 +66,30 @@ public void getWhenUsingMinimalConfigurationThenRedirectsToLogin() throws Except // @formatter:on } + @Test + public void getWhenUsingMinimalAuthorizationManagerThenRedirectsToLogin() throws Exception { + this.spring.configLocations(this.xml("MinimalAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + // @formatter:on + } + + @Test + public void getWhenUsingAuthorizationManagerThenRedirectsToLogin() throws Exception { + this.spring.configLocations(this.xml("AuthorizationManager")).autowire(); + AuthorizationManager authorizationManager = this.spring.getContext() + .getBean(AuthorizationManager.class); + given(authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + // @formatter:on + verify(authorizationManager).check(any(), any()); + } + @Test public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter() throws Exception { this.spring.configLocations(this.xml("Minimal")).autowire(); diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java index c399b1994ab..088bd5334d8 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -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. @@ -48,6 +48,7 @@ * @author Tim Ysewyn * @author Josh Cummings * @author Rafiullah Hamedy + * @author Marcus Da Coregio */ @ExtendWith(SpringTestContextExtension.class) public class HttpHeadersConfigTests { @@ -733,6 +734,53 @@ public void requestWhenReferrerPolicyConfiguredWithSameOriginThenRespondsWithSam // @formatter:on } + @Test + public void requestWhenCrossOriginOpenerPolicyWithSameOriginAllowPopupsThenRespondsWithSameOriginAllowPopups() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginOpenerPolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Opener-Policy", "same-origin-allow-popups")); + // @formatter:on + } + + @Test + public void requestWhenCrossOriginEmbedderPolicyWithRequireCorpThenRespondsWithRequireCorp() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginEmbedderPolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Embedder-Policy", "require-corp")); + // @formatter:on + } + + @Test + public void requestWhenCrossOriginResourcePolicyWithSameOriginThenRespondsWithSameOrigin() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginResourcePolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Resource-Policy", "same-origin")); + // @formatter:on + } + + @Test + public void requestWhenCrossOriginPoliciesRespondsCrossOriginPolicies() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginPolicies")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Opener-Policy", "same-origin")) + .andExpect(header().string("Cross-Origin-Embedder-Policy", "require-corp")) + .andExpect(header().string("Cross-Origin-Resource-Policy", "same-origin")); + // @formatter:on + } + private static ResultMatcher includesDefaults() { return includes(defaultHeaders); } diff --git a/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java b/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java index 2ebd408395f..09e2a09557c 100644 --- a/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.test.web.servlet.MockMvc; @@ -37,6 +38,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.ConfigurableWebApplicationContext; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -75,6 +77,21 @@ public void requestWhenMethodIsSpecifiedThenItIsNotGivenPriority() throws Except // @formatter:on } + /** + * sec-2256 + */ + @Test + public void requestWhenMethodIsSpecifiedAndAuthorizationManagerThenItIsNotGivenPriority() throws Exception { + this.spring.configLocations(this.xml("Sec2256AuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(post("/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(get("/path").with(userCredentials())) + .andExpect(status().isOk()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + /** * sec-2355 */ @@ -91,6 +108,23 @@ public void requestWhenUsingPatchThenAuthorizesRequestsAccordingly() throws Exce // @formatter:on } + /** + * sec-2355 + */ + @Test + public void requestWhenUsingPatchAndAuthorizationManagerThenAuthorizesRequestsAccordingly() throws Exception { + this.spring.configLocations(this.xml("PatchMethodAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(patch("/path").with(userCredentials())) + .andExpect(status().isForbidden()); + this.mvc.perform(patch("/path").with(adminCredentials())) + .andExpect(status().isOk()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + @Test public void requestWhenUsingHasAnyRoleThenAuthorizesRequestsAccordingly() throws Exception { this.spring.configLocations(this.xml("HasAnyRole")).autowire(); @@ -102,6 +136,18 @@ public void requestWhenUsingHasAnyRoleThenAuthorizesRequestsAccordingly() throws // @formatter:on } + @Test + public void requestWhenUsingHasAnyRoleAndAuthorizationManagerThenAuthorizesRequestsAccordingly() throws Exception { + this.spring.configLocations(this.xml("HasAnyRoleAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(get("/path").with(adminCredentials())) + .andExpect(status().isForbidden()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + /** * sec-2059 */ @@ -118,6 +164,24 @@ public void requestWhenUsingPathVariablesThenAuthorizesRequestsAccordingly() thr // @formatter:on } + /** + * sec-2059 + */ + @Test + public void requestWhenUsingPathVariablesAndAuthorizationManagerThenAuthorizesRequestsAccordingly() + throws Exception { + this.spring.configLocations(this.xml("PathVariablesAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/path/user/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(get("/path/otheruser/path").with(userCredentials())) + .andExpect(status().isForbidden()); + this.mvc.perform(get("/path").with(userCredentials())) + .andExpect(status().isForbidden()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + /** * gh-3786 */ @@ -134,6 +198,24 @@ public void requestWhenUsingCamelCasePathVariablesThenAuthorizesRequestsAccordin // @formatter:on } + /** + * gh-3786 + */ + @Test + public void requestWhenUsingCamelCasePathVariablesAndAuthorizationManagerThenAuthorizesRequestsAccordingly() + throws Exception { + this.spring.configLocations(this.xml("CamelCasePathVariablesAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/path/user/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(get("/path/otheruser/path").with(userCredentials())) + .andExpect(status().isForbidden()); + this.mvc.perform(get("/PATH/user/path").with(userCredentials())) + .andExpect(status().isForbidden()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + /** * sec-2059 */ @@ -148,6 +230,22 @@ public void requestWhenUsingPathVariablesAndTypeConversionThenAuthorizesRequests // @formatter:on } + /** + * sec-2059 + */ + @Test + public void requestWhenUsingPathVariablesAndTypeConversionAndAuthorizationManagerThenAuthorizesRequestsAccordingly() + throws Exception { + this.spring.configLocations(this.xml("TypeConversionPathVariablesAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/path/1/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(get("/path/2/path").with(userCredentials())) + .andExpect(status().isForbidden()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + @Test public void requestWhenUsingMvcMatchersThenAuthorizesRequestsAccordingly() throws Exception { this.spring.configLocations(this.xml("MvcMatchers")).autowire(); @@ -156,6 +254,15 @@ public void requestWhenUsingMvcMatchersThenAuthorizesRequestsAccordingly() throw this.mvc.perform(get("/path/")).andExpect(status().isUnauthorized()); } + @Test + public void requestWhenUsingMvcMatchersAndAuthorizationManagerThenAuthorizesRequestsAccordingly() throws Exception { + this.spring.configLocations(this.xml("MvcMatchersAuthorizationManager")).autowire(); + this.mvc.perform(get("/path")).andExpect(status().isUnauthorized()); + this.mvc.perform(get("/path.html")).andExpect(status().isUnauthorized()); + this.mvc.perform(get("/path/")).andExpect(status().isUnauthorized()); + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + @Test public void requestWhenUsingMvcMatchersAndPathVariablesThenAuthorizesRequestsAccordingly() throws Exception { this.spring.configLocations(this.xml("MvcMatchersPathVariables")).autowire(); @@ -169,6 +276,21 @@ public void requestWhenUsingMvcMatchersAndPathVariablesThenAuthorizesRequestsAcc // @formatter:on } + @Test + public void requestWhenUsingMvcMatchersAndPathVariablesAndAuthorizationManagerThenAuthorizesRequestsAccordingly() + throws Exception { + this.spring.configLocations(this.xml("MvcMatchersPathVariablesAuthorizationManager")).autowire(); + // @formatter:off + this.mvc.perform(get("/path/user/path").with(userCredentials())) + .andExpect(status().isOk()); + this.mvc.perform(get("/path/otheruser/path").with(userCredentials())) + .andExpect(status().isForbidden()); + this.mvc.perform(get("/PATH/user/path").with(userCredentials())) + .andExpect(status().isForbidden()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + @Test public void requestWhenUsingMvcMatchersAndServletPathThenAuthorizesRequestsAccordingly() throws Exception { this.spring.configLocations(this.xml("MvcMatchersServletPath")).autowire(); @@ -185,30 +307,72 @@ public void requestWhenUsingMvcMatchersAndServletPathThenAuthorizesRequestsAccor // @formatter:on } + @Test + public void requestWhenUsingMvcMatchersAndServletPathAndAuthorizationManagerThenAuthorizesRequestsAccordingly() + throws Exception { + this.spring.configLocations(this.xml("MvcMatchersServletPathAuthorizationManager")).autowire(); + MockServletContext servletContext = mockServletContext("/spring"); + ConfigurableWebApplicationContext context = this.spring.getContext(); + context.setServletContext(servletContext); + // @formatter:off + this.mvc.perform(get("/spring/path").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + this.mvc.perform(get("/spring/path.html").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + this.mvc.perform(get("/spring/path/").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + // @formatter:on + assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull(); + } + @Test public void configureWhenUsingAntMatcherAndServletPathThenThrowsException() { assertThatExceptionOfType(BeanDefinitionParsingException.class) .isThrownBy(() -> this.spring.configLocations(this.xml("AntMatcherServletPath")).autowire()); } + @Test + public void configureWhenUsingAntMatcherAndServletPathAndAuthorizationManagerThenThrowsException() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy( + () -> this.spring.configLocations(this.xml("AntMatcherServletPathAuthorizationManager")).autowire()); + } + @Test public void configureWhenUsingRegexMatcherAndServletPathThenThrowsException() { assertThatExceptionOfType(BeanDefinitionParsingException.class) .isThrownBy(() -> this.spring.configLocations(this.xml("RegexMatcherServletPath")).autowire()); } + @Test + public void configureWhenUsingRegexMatcherAndServletPathAndAuthorizationManagerThenThrowsException() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy( + () -> this.spring.configLocations(this.xml("RegexMatcherServletPathAuthorizationManager")).autowire()); + } + @Test public void configureWhenUsingCiRegexMatcherAndServletPathThenThrowsException() { assertThatExceptionOfType(BeanDefinitionParsingException.class) .isThrownBy(() -> this.spring.configLocations(this.xml("CiRegexMatcherServletPath")).autowire()); } + @Test + public void configureWhenUsingCiRegexMatcherAndServletPathAndAuthorizationManagerThenThrowsException() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> this.spring + .configLocations(this.xml("CiRegexMatcherServletPathAuthorizationManager")).autowire()); + } + @Test public void configureWhenUsingDefaultMatcherAndServletPathThenThrowsException() { assertThatExceptionOfType(BeanDefinitionParsingException.class) .isThrownBy(() -> this.spring.configLocations(this.xml("DefaultMatcherServletPath")).autowire()); } + @Test + public void configureWhenUsingDefaultMatcherAndServletPathAndAuthorizationManagerThenThrowsException() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> this.spring + .configLocations(this.xml("DefaultMatcherServletPathAuthorizationManager")).autowire()); + } + private static RequestPostProcessor adminCredentials() { return httpBasic("admin", "password"); } diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index 0fc87cfe8cb..0cffa002ae2 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -103,6 +103,7 @@ import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; @@ -121,7 +122,10 @@ import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509; @@ -462,6 +466,36 @@ public void getWhenAuthenticatingThenConsultsCustomSecurityContextRepository() t any(HttpServletResponse.class)); } + @Test + public void getWhenExplicitSaveAndRepositoryAndAuthenticatingThenConsultsCustomSecurityContextRepository() + throws Exception { + this.spring.configLocations(xml("ExplicitSaveAndExplicitRepository")).autowire(); + SecurityContextRepository repository = this.spring.getContext().getBean(SecurityContextRepository.class); + SecurityContext context = new SecurityContextImpl(new TestingAuthenticationToken("user", "password")); + given(repository.loadContext(any(HttpServletRequest.class))).willReturn(() -> context); + // @formatter:off + MvcResult result = this.mvc.perform(formLogin()) + .andExpect(status().is3xxRedirection()) + .andReturn(); + // @formatter:on + verify(repository, atLeastOnce()).saveContext(any(SecurityContext.class), any(HttpServletRequest.class), + any(HttpServletResponse.class)); + } + + @Test + public void getWhenExplicitSaveAndExplicitSaveAndAuthenticatingThenConsultsCustomSecurityContextRepository() + throws Exception { + this.spring.configLocations(xml("ExplicitSave")).autowire(); + SecurityContextRepository repository = this.spring.getContext().getBean(SecurityContextRepository.class); + // @formatter:off + MvcResult result = this.mvc.perform(formLogin()) + .andExpect(status().is3xxRedirection()) + .andReturn(); + // @formatter:on + assertThat(repository.loadContext(new HttpRequestResponseHolder(result.getRequest(), result.getResponse())) + .getAuthentication()).isNotNull(); + } + @Test public void getWhenUsingInterceptUrlExpressionsThenAuthorizesAccordingly() throws Exception { this.spring.configLocations(xml("InterceptUrlExpressions")).autowire(); @@ -509,6 +543,28 @@ public void configureWhenUsingDisableUrlRewritingThenRedirectIsNotEncodedByRespo assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); } + @Test + public void configureWhenUsingDisableUrlRewritingAndCustomRepositoryThenRedirectIsNotEncodedByResponse() + throws IOException, ServletException { + this.spring.configLocations(xml("DisableUrlRewriting-NullSecurityContextRepository")).autowire(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse responseToSpy = spy(new MockHttpServletResponse()); + FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class); + proxy.doFilter(request, responseToSpy, (req, resp) -> { + HttpServletResponse httpResponse = (HttpServletResponse) resp; + httpResponse.encodeUrl("/"); + httpResponse.encodeURL("/"); + httpResponse.encodeRedirectUrl("/"); + httpResponse.encodeRedirectURL("/"); + httpResponse.getWriter().write("encodeRedirect"); + }); + verify(responseToSpy, never()).encodeRedirectURL(any()); + verify(responseToSpy, never()).encodeRedirectUrl(any()); + verify(responseToSpy, never()).encodeURL(any()); + verify(responseToSpy, never()).encodeUrl(any()); + assertThat(responseToSpy.getContentAsString()).isEqualTo("encodeRedirect"); + } + @Test public void configureWhenUserDetailsServiceInParentContextThenLocatesSuccessfully() { assertThatExceptionOfType(BeansException.class).isThrownBy( @@ -724,6 +780,7 @@ private void assertThatFiltersMatchExpectedAutoConfigList() { private void assertThatFiltersMatchExpectedAutoConfigList(String url) { Iterator filters = getFilters(url).iterator(); + assertThat(filters.next()).isInstanceOf(DisableEncodeUrlFilter.class); assertThat(filters.next()).isInstanceOf(SecurityContextPersistenceFilter.class); assertThat(filters.next()).isInstanceOf(WebAsyncManagerIntegrationFilter.class); assertThat(filters.next()).isInstanceOf(HeaderWriterFilter.class); diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java new file mode 100644 index 00000000000..d4ebbd68023 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java @@ -0,0 +1,310 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.core.Saml2Utils; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationRequestContexts; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link Saml2LoginBeanDefinitionParser} + * + * @author Marcus da Coregio + */ +@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) +@SecurityTestExecutionListeners +public class Saml2LoginBeanDefinitionParserTests { + + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests"; + + private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4="; + + private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired(required = false) + private RequestCache requestCache; + + @Autowired(required = false) + private AuthenticationFailureHandler authenticationFailureHandler; + + @Autowired(required = false) + private AuthenticationSuccessHandler authenticationSuccessHandler; + + @Autowired(required = false) + private RelyingPartyRegistrationRepository repository; + + @Autowired(required = false) + private ApplicationListener authenticationSuccessListener; + + @Autowired(required = false) + private AuthenticationConverter authenticationConverter; + + @Autowired(required = false) + private Saml2AuthenticationRequestResolver authenticationRequestResolver; + + @Autowired(required = false) + private Saml2AuthenticationRequestRepository authenticationRequestRepository; + + @Autowired(required = false) + private ApplicationContext applicationContext; + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenSingleRelyingPartyRegistrationThenAutoRedirect() throws Exception { + this.spring.configLocations(this.xml("SingleRelyingPartyRegistration")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/saml2/authenticate/one")); + // @formatter:on + verify(this.requestCache).saveRequest(any(), any()); + } + + @Test + public void requestWhenMultiRelyingPartyRegistrationThenRedirectToLoginWithRelyingParties() throws Exception { + this.spring.configLocations(this.xml("MultiRelyingPartyRegistration")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + // @formatter:on + } + + @Test + public void requestLoginWhenMultiRelyingPartyRegistrationThenReturnLoginPageWithRelyingParties() throws Exception { + this.spring.configLocations(this.xml("MultiRelyingPartyRegistration")).autowire(); + // @formatter:off + MvcResult mvcResult = this.mvc.perform(get("/login")) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + // @formatter:on + String pageContent = mvcResult.getResponse().getContentAsString(); + assertThat(pageContent).contains("two"); + assertThat(pageContent).contains("one"); + } + + @Test + public void authenticateWhenAuthenticationResponseNotValidThenThrowAuthenticationException() throws Exception { + this.spring.configLocations(this.xml("SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler")) + .autowire(); + this.mvc.perform(get("/login/saml2/sso/one").param(Saml2ParameterNames.SAML_RESPONSE, "samlResponse123")); + ArgumentCaptor exceptionCaptor = ArgumentCaptor + .forClass(AuthenticationException.class); + verify(this.authenticationFailureHandler).onAuthenticationFailure(any(), any(), exceptionCaptor.capture()); + AuthenticationException exception = exceptionCaptor.getValue(); + assertThat(exception).isInstanceOf(Saml2AuthenticationException.class); + assertThat(((Saml2AuthenticationException) exception).getSaml2Error().getErrorCode()) + .isEqualTo("invalid_response"); + } + + @Test + public void authenticateWhenAuthenticationResponseValidThenAuthenticate() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository")).autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + // @formatter:off + this.mvc.perform(post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()).param(Saml2ParameterNames.SAML_RESPONSE, SIGNED_RESPONSE)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().is2xxSuccessful()); + // @formatter:on + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(this.authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authentication = authenticationCaptor.getValue(); + assertThat(authentication.getPrincipal()).isInstanceOf(Saml2AuthenticatedPrincipal.class); + } + + @Test + public void authenticateWhenAuthenticationResponseValidThenAuthenticationSuccessEventPublished() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository")).autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + // @formatter:off + this.mvc.perform(post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()).param(Saml2ParameterNames.SAML_RESPONSE, SIGNED_RESPONSE)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().is2xxSuccessful()); + // @formatter:on + verify(this.authenticationSuccessListener).onApplicationEvent(any(AuthenticationSuccessEvent.class)); + } + + @Test + public void authenticateWhenCustomAuthenticationConverterThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter")) + .autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); + given(this.authenticationConverter.convert(any(HttpServletRequest.class))) + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); + // @formatter:off + MockHttpServletRequestBuilder request = post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()) + .param("SAMLResponse", SIGNED_RESPONSE); + // @formatter:on + this.mvc.perform(request).andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")); + verify(this.authenticationConverter).convert(any(HttpServletRequest.class)); + } + + @Test + public void authenticateWhenCustomAuthenticationManagerThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthenticationManager")) + .autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + AuthenticationManager authenticationManager = this.applicationContext.getBean("customAuthenticationManager", + AuthenticationManager.class); + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); + given(authenticationManager.authenticate(any())) + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); + // @formatter:off + MockHttpServletRequestBuilder request = post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()) + .param("SAMLResponse", SIGNED_RESPONSE); + // @formatter:on + this.mvc.perform(request).andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")); + verify(authenticationManager).authenticate(any()); + } + + @Test + public void authenticationRequestWhenCustomAuthenticationRequestContextResolverThenUses() throws Exception { + this.spring + .configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver")) + .autowire(); + Saml2RedirectAuthenticationRequest request = Saml2RedirectAuthenticationRequest + .withAuthenticationRequestContext( + TestSaml2AuthenticationRequestContexts.authenticationRequestContext().build()) + .samlRequest("request").authenticationRequestUri(IDP_SSO_URL).build(); + given(this.authenticationRequestResolver.resolve(any(HttpServletRequest.class))).willReturn(request); + this.mvc.perform(get("/saml2/authenticate/registration-id")).andExpect(status().isFound()); + verify(this.authenticationRequestResolver).resolve(any(HttpServletRequest.class)); + } + + @Test + public void authenticationRequestWhenCustomAuthnRequestRepositoryThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository")) + .autowire(); + given(this.repository.findByRegistrationId(anyString())) + .willReturn(TestRelyingPartyRegistrations.relyingPartyRegistration().build()); + MockHttpServletRequestBuilder request = get("/saml2/authenticate/registration-id"); + this.mvc.perform(request).andExpect(status().isFound()); + verify(this.authenticationRequestRepository).saveAuthenticationRequest( + any(AbstractSaml2AuthenticationRequest.class), any(HttpServletRequest.class), + any(HttpServletResponse.class)); + } + + @Test + public void authenticateWhenCustomAuthnRequestRepositoryThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository")) + .autowire(); + RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); + given(this.repository.findByRegistrationId(anyString())) + .willReturn(TestRelyingPartyRegistrations.relyingPartyRegistration().build()); + MockHttpServletRequestBuilder request = post("/login/saml2/sso/registration-id").param("SAMLResponse", + SIGNED_RESPONSE); + this.mvc.perform(request); + verify(this.authenticationRequestRepository).loadAuthenticationRequest(any(HttpServletRequest.class)); + verify(this.authenticationRequestRepository).removeAuthenticationRequest(any(HttpServletRequest.class), + any(HttpServletResponse.class)); + } + + @Test + public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() { + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> this.spring.configLocations(this.xml("WithCustomLoginProcessingUrl")).autowire()) + .withMessageContaining("loginProcessingUrl must contain {registrationId} path variable"); + } + + @Test + public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate() + throws Exception { + this.spring.configLocations(this.xml("WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter")) + .autowire(); + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); + given(this.authenticationConverter.convert(any(HttpServletRequest.class))) + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); + // @formatter:off + MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE); + // @formatter:on + this.mvc.perform(request).andExpect(redirectedUrl("/")); + verify(this.authenticationConverter).convert(any(HttpServletRequest.class)); + } + + private RelyingPartyRegistration relyingPartyRegistrationWithVerifyingCredential() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); + given(this.repository.findByRegistrationId(anyString())).willReturn(relyingPartyRegistration); + return relyingPartyRegistration; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java new file mode 100644 index 00000000000..4dc54b43f0d --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java @@ -0,0 +1,350 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.http; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.saml2.core.Saml2Utils; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link Saml2LogoutBeanDefinitionParser} + * + * @author Marcus da Coregio + */ +@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) +@SecurityTestExecutionListeners +public class Saml2LogoutBeanDefinitionParserTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + private final Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository(); + + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests"; + + String apLogoutRequest = "nZFBa4MwGIb/iuQeE2NTXFDLQAaC26Hrdtgt1dQFNMnyxdH9+zlboeyww275SN7nzcOX787jEH0qD9qaAiUxRZEyre206Qv0cnjAGdqVOchxYE40trdT2KuPSUGI5qQBcbkq0OSNsBI0CCNHBSK04vn+sREspsJ5G2xrBxRVc1AbGZa29xAcCEK8i9VZjm5QsfU9GZYWsoCJv5ShqK4K1Ow5p5LyU4aP6XaLN3cpw9mGctydjrxNaZt1XM5vASZVGwjShAIxyhJMU8z4gSWCM8GSmDH+hqLX1Xv+JLpaiiXsb+3+lpMAyv8IoVI6rEzQ4QvrLie3uBX+NMfr6l/waT6t0AumvI6/FlN+Aw=="; + + String apLogoutRequestSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + + String apLogoutRequestRelayState = "33591874-b123-4f2c-ab0d-2d0d84aa8b56"; + + String apLogoutRequestSignature = "oKqdzrmn2YAqXcwkow2lzRXr5PNHm0s/gWsRnaZYhC+Oq5ekK5uIKQYvtmNR94HJjDe1VRs+vVQCYivgdoTzBV2ZlffTXZmYsCsY9q4jbCWR6R5CbhU73/MkKQsPcyVvMhNYxnDYapIlxDsfoZNTboDEz3GM+HRoGRfl9emCXY0lPRYwqC4kpu7oMDBkafR0A09jPIxFuNpqlLPwUxL9m+DGkvDK3mFDN1xJcgZaK73HcuJe7Qh4huOrKNFetwc5EvqfiwgiWF6sfq9A+rZBfCIYo10NNLY7fNQAR2IqwcKtawHgTGWbeshRyFrwVYMR64EnClfxUHsHKf5kiZ2dlw=="; + + String apLogoutResponse = "fZHRa4MwEMb/Fcl7jEadGqplrAwK3Uvb9WFvZ4ydoInk4uj++1nXbmWMvhwcd9/3Jb9bLE99530oi63RBQn9gHhKS1O3+liQ1/0zzciyXCD0HR/ExhzN6LYKB6NReZNUo/ieFWS0WhjAFoWGXqFwUuweXzaC+4EYrHFGmo54K4Wu1eDmuHfnBhSM2cFXJ+iHTvnGHlk3x7DZmNlLGvHWq4Jstk0GUSjjiIZJI2lcpQnNeRLTAOo4fwCeQg3Trr6+cm/OqmnWVHECVGWQ0jgCSatsKvXUxhFvZF7xSYU4qrVGB9oVhAc8pEFEebLnkeBc8NyPePpGvMOV1/Q3cqEjZrG9hXKfCSAqe+ZAShio0q51n7StF+zW7gf9zoEb8U/7ZGrlHaAb1f0onLfFbpRSIRJWXkJ+bdm/Fy6/AA=="; + + String apLogoutResponseSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + + String apLogoutResponseRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315"; + + String apLogoutResponseSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q=="; + + String rpLogoutRequest = "nZFBa4MwGIb/iuQeY6NlGtQykIHgdui6HXaLmrqAJlm+OLp/v0wrlB122CXkI3mfNw/JD5dpDD6FBalVgXZhhAKhOt1LNRTo5fSAU3Qoc+DTSA1r9KBndxQfswAX+KQCth4VaLaKaQ4SmOKTAOY69nz/2DAaRsxY7XSnRxRUPigVd0vbu3MGGCHchOLCJzOKUNuBjEsLWcDErmUoqKsCNcc+yc5tsudYpPwOJzHvcJv6pfdjEtNzl7XU3wWYRa3AceUKRCO6w1GM6f5EY0Ypo1lIk+gNBa+bt38kulqyJWxv7f6W4wDC/gih0hoslJPuC8s+J7e4Df7k43X1L/jsdxt0xZTX8dfHlN8="; + + String rpLogoutRequestId = "LRd49fb45a-e8a7-43ac-b8ac-d8a7432fc9b2"; + + String rpLogoutRequestRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315"; + + String rpLogoutRequestSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q=="; + + @Autowired(required = false) + private RelyingPartyRegistrationRepository repository; + + @Autowired + private MockMvc mvc; + + private Saml2Authentication saml2User; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + public void setup() { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("registration-id"); + this.saml2User = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + this.request = new MockHttpServletRequest("POST", ""); + this.request.setServletPath("/login/saml2/sso/test-rp"); + this.response = new MockHttpServletResponse(); + } + + @Test + public void logoutWhenLogoutSuccessHandlerAndNotSaml2LoginThenDefaultLogoutSuccessHandler() throws Exception { + this.spring.configLocations(this.xml("LogoutSuccessHandler")).autowire(); + TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password"); + MvcResult result = this.mvc.perform(post("/logout").with(authentication(user)).with(csrf())) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).isEqualTo("/logoutSuccessEndpoint"); + } + + @Test + public void saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf())) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/request"); + } + + @Test + public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")); + } + + @Test + public void saml2LogoutWhenMissingCsrfThen403() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + this.mvc.perform(post("/logout").with(authentication(this.saml2User))).andExpect(status().isForbidden()); + } + + @Test + public void saml2LogoutWhenGetThenDefaultLogoutPage() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + MvcResult result = this.mvc.perform(get("/logout").with(authentication(this.saml2User))) + .andExpect(status().isOk()).andReturn(); + assertThat(result.getResponse().getContentAsString()).contains("Are you sure you want to log out?"); + } + + @Test + public void saml2LogoutWhenPutOrDeleteThen404() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + this.mvc.perform(put("/logout").with(authentication(this.saml2User)).with(csrf())) + .andExpect(status().isNotFound()); + this.mvc.perform(delete("/logout").with(authentication(this.saml2User)).with(csrf())) + .andExpect(status().isNotFound()); + } + + @Test + public void saml2LogoutWhenNoRegistrationThen401() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("wrong"); + Saml2Authentication authentication = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + this.mvc.perform(post("/logout").with(authentication(authentication)).with(csrf())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void saml2LogoutWhenCsrfDisabledAndNoAuthenticationThenFinalRedirect() throws Exception { + this.spring.configLocations(this.xml("CsrfDisabled-MockLogoutSuccessHandler")).autowire(); + this.mvc.perform(post("/logout")); + LogoutSuccessHandler logoutSuccessHandler = this.spring.getContext().getBean(LogoutSuccessHandler.class); + verify(logoutSuccessHandler).onLogoutSuccess(any(), any(), any()); + } + + @Test + public void saml2LogoutWhenCustomLogoutRequestResolverThenUses() throws Exception { + this.spring.configLocations(this.xml("CustomComponents")).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest); + this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf())); + verify(getBean(Saml2LogoutRequestResolver.class)).resolve(any(), any()); + } + + @Test + public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("get"); + Saml2Authentication user = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + MvcResult result = this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .param("Signature", this.apLogoutRequestSignature).with(samlQueryString()).with(authentication(user))) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/response"); + } + + @Test + public void saml2LogoutRequestWhenNoRegistrationThen400() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("wrong"); + Saml2Authentication user = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .param("Signature", this.apLogoutRequestSignature).with(authentication(user))) + .andExpect(status().isBadRequest()); + } + + @Test + public void saml2LogoutRequestWhenInvalidSamlRequestThen401() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .with(authentication(this.saml2User))).andExpect(status().isUnauthorized()); + } + + @Test + public void saml2LogoutRequestWhenCustomLogoutRequestHandlerThenUses() throws Exception { + this.spring.configLocations(this.xml("CustomComponents")).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id"); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + logoutRequest.setIssueInstant(Instant.now()); + given(getBean(Saml2LogoutRequestValidator.class).validate(any())) + .willReturn(Saml2LogoutValidatorResult.success()); + Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration).build(); + given(getBean(Saml2LogoutResponseResolver.class).resolve(any(), any())).willReturn(logoutResponse); + this.mvc.perform( + post("/logout/saml2/slo").param("SAMLRequest", "samlRequest").with(authentication(this.saml2User))) + .andReturn(); + verify(getBean(Saml2LogoutRequestValidator.class)).validate(any()); + verify(getBean(Saml2LogoutResponseResolver.class)).resolve(any(), any()); + } + + @Test + public void saml2LogoutResponseWhenDefaultsThenRedirects() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("get"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response); + this.request.setParameter("RelayState", logoutRequest.getRelayState()); + assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNotNull(); + this.mvc.perform(get("/logout/saml2/slo").session(((MockHttpSession) this.request.getSession())) + .param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState) + .param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature) + .with(samlQueryString())).andExpect(status().isFound()).andExpect(redirectedUrl("/login?logout")); + assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull(); + } + + @Test + public void saml2LogoutResponseWhenInvalidSamlResponseThen401() throws Exception { + this.spring.configLocations(this.xml("Default")).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response); + String deflatedApLogoutResponse = Saml2Utils.samlEncode( + Saml2Utils.samlInflate(Saml2Utils.samlDecode(this.apLogoutResponse)).getBytes(StandardCharsets.UTF_8)); + this.mvc.perform(post("/logout/saml2/slo").session((MockHttpSession) this.request.getSession()) + .param("SAMLResponse", deflatedApLogoutResponse).param("RelayState", this.rpLogoutRequestRelayState) + .param("SigAlg", this.apLogoutRequestSigAlg).param("Signature", this.apLogoutResponseSignature) + .with(samlQueryString())).andExpect(status().reason(containsString("invalid_signature"))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws Exception { + this.spring.configLocations(this.xml("CustomComponents")).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("get"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + given(getBean(Saml2LogoutRequestRepository.class).removeLogoutRequest(any(), any())).willReturn(logoutRequest); + given(getBean(Saml2LogoutResponseValidator.class).validate(any())) + .willReturn(Saml2LogoutValidatorResult.success()); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLResponse", "samlResponse")).andReturn(); + verify(getBean(Saml2LogoutResponseValidator.class)).validate(any()); + } + + private T getBean(Class clazz) { + return this.spring.getContext().getBean(clazz); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + + private SamlQueryStringRequestPostProcessor samlQueryString() { + return new SamlQueryStringRequestPostProcessor(); + } + + static class SamlQueryStringRequestPostProcessor implements RequestPostProcessor { + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry entries : request.getParameterMap().entrySet()) { + builder.queryParam(entries.getKey(), + UriUtils.encode(entries.getValue()[0], StandardCharsets.ISO_8859_1)); + } + request.setQueryString(builder.build(true).toUriString().substring(1)); + return request; + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParserTests.java index c4d08dc5295..affbe760ab7 100644 --- a/config/src/test/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -67,7 +67,7 @@ */ public class GlobalMethodSecurityBeanDefinitionParserTests { - private final UsernamePasswordAuthenticationToken bob = new UsernamePasswordAuthenticationToken("bob", + private final UsernamePasswordAuthenticationToken bob = UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword"); private AbstractXmlApplicationContext appContext; @@ -106,7 +106,8 @@ public void targetShouldPreventProtectedMethodInvocationWithNoContext() { @Test public void targetShouldAllowProtectedMethodInvocationWithCorrectRole() { loadContext(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); SecurityContextHolder.getContext().setAuthentication(token); this.target.someUserMethod1(); // SEC-1213. Check the order @@ -153,8 +154,8 @@ public void worksWithAspectJAutoproxy() { + ""); // @formatter:on UserDetailsService service = (UserDetailsService) this.appContext.getBean("myUserService"); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); SecurityContextHolder.getContext().setAuthentication(token); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> service.loadUserByUsername("notused")); } @@ -170,7 +171,7 @@ public void supportsMethodArgumentsInPointcut() { + ConfigTestUtils.AUTH_PROVIDER_XML); // @formatter:on SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("user", "password")); + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); this.target = (BusinessService) this.appContext.getBean("target"); // someOther(int) should not be matched by someOther(String), but should require // ROLE_USER @@ -198,7 +199,7 @@ public void supportsBooleanPointcutExpressions() { assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) .isThrownBy(() -> this.target.someOther(0)); SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("user", "password")); + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); this.target.someOther(0); } @@ -219,8 +220,8 @@ public void worksWithoutTargetOrClass() { + "" + ConfigTestUtils.AUTH_PROVIDER_XML); // @formatter:on - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); SecurityContextHolder.getContext().setAuthentication(token); this.target = (BusinessService) this.appContext.getBean("businessService"); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.target::someUserMethod1); @@ -384,7 +385,7 @@ public void supportsExternalMetadataSource() { Foo foo = (Foo) this.appContext.getBean("target"); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> foo.foo(new SecurityConfig("A"))); SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("admin", "password")); + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("admin", "password")); foo.foo(new SecurityConfig("A")); } @@ -405,7 +406,7 @@ public void supportsCustomAuthenticationManager() { Foo foo = (Foo) this.appContext.getBean("target"); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> foo.foo(new SecurityConfig("A"))); SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("admin", "password")); + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("admin", "password")); foo.foo(new SecurityConfig("A")); } diff --git a/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java b/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java index 15e375b6183..e7dd9331005 100644 --- a/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java +++ b/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -91,16 +91,16 @@ public void targetShouldPreventProtectedMethodInvocationWithNoContext() { @Test public void targetShouldAllowProtectedMethodInvocationWithCorrectRole() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_USER")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_USER")); SecurityContextHolder.getContext().setAuthentication(token); this.target.doSomething(); } @Test public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); SecurityContextHolder.getContext().setAuthentication(token); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.target::doSomething); } diff --git a/config/src/test/java/org/springframework/security/config/method/Jsr250AnnotationDrivenBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/Jsr250AnnotationDrivenBeanDefinitionParserTests.java index 0e699b73664..654a01b2f39 100644 --- a/config/src/test/java/org/springframework/security/config/method/Jsr250AnnotationDrivenBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/method/Jsr250AnnotationDrivenBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -67,32 +67,32 @@ public void targetShouldPreventProtectedMethodInvocationWithNoContext() { @Test public void permitAllShouldBeDefaultAttribute() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_USER")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_USER")); SecurityContextHolder.getContext().setAuthentication(token); this.target.someOther(0); } @Test public void targetShouldAllowProtectedMethodInvocationWithCorrectRole() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_USER")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_USER")); SecurityContextHolder.getContext().setAuthentication(token); this.target.someUserMethod1(); } @Test public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE")); SecurityContextHolder.getContext().setAuthentication(token); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.target::someAdminMethod); } @Test public void hasAnyRoleAddsDefaultPrefix() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_USER")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_USER")); SecurityContextHolder.getContext().setAuthentication(token); this.target.rolesAllowedUser(); } diff --git a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java index c458528f2ef..adcb526a727 100644 --- a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -59,7 +59,7 @@ public class MethodSecurityBeanDefinitionParserTests { private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests"; - private final UsernamePasswordAuthenticationToken bob = new UsernamePasswordAuthenticationToken("bob", + private final UsernamePasswordAuthenticationToken bob = UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword"); @Autowired(required = false) diff --git a/config/src/test/java/org/springframework/security/config/method/SecuredAnnotationDrivenBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/SecuredAnnotationDrivenBeanDefinitionParserTests.java index 4b760367dfe..4995df4369a 100644 --- a/config/src/test/java/org/springframework/security/config/method/SecuredAnnotationDrivenBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/method/SecuredAnnotationDrivenBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -73,16 +73,16 @@ public void targetShouldPreventProtectedMethodInvocationWithNoContext() { @Test public void targetShouldAllowProtectedMethodInvocationWithCorrectRole() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_USER")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_USER")); SecurityContextHolder.getContext().setAuthentication(token); this.target.someUserMethod1(); } @Test public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_SOMEOTHER")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_SOMEOTHER")); SecurityContextHolder.getContext().setAuthentication(token); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.target::someAdminMethod); } diff --git a/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java new file mode 100644 index 00000000000..f90a2be1b5f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.saml2; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RelyingPartyRegistrationsBeanDefinitionParser}. + * + * @author Marcus da Coregio + */ +@ExtendWith(SpringTestContextExtension.class) +public class RelyingPartyRegistrationsBeanDefinitionParserTests { + + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests"; + + // @formatter:off + private static final String METADATA_LOCATION_XML_CONFIG = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n"; + // @formatter:on + + // @formatter:off + private static final String METADATA_RESPONSE = "\n" + + "\n" + + " \n" + + " \n" + + " qIGOB+m2Kuq9Vp6F9qs/EFvFzuo6qEGukjICPyVAkjk=NgKak4k9LBAqbi8Za8ALUXW1l4npZ4+MOf8jhmpePDP3msbzjeKkkWFgxx+ILLJYwZzVWd3l028xm2l+SBOwoYRKJ670NgcdSdj6plBTGiZ5NXsXrX5M0zmgvAShREgjth/BKTUct5UVJOTqIxOPwBuCnj+Nn1+QUtY9ekPLrM0O2i+g1wckKaP6D7N+uVBwNgZGoOj5bZ082G7QXRX6Jo0925uKczAIKdIiBbMeKa/0phS2L97AkgQRGi2+j8V66TaDWuDSwd9hA2qzCwjsNui4DVLBwP0/LvgUdcu8g7JBIZ1yTddfByefOTVsU7UuZXkYEn4jU2ouk+u5klSo3Q==\n" + + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n" + + " \n" + + " \n" + + " \n" + + " John\n" + + " Doe\n" + + " john@doe.com\n" + + " \n" + + "\n"; + // @formatter:on + + @Autowired + private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + + public final SpringTestContext spring = new SpringTestContext(this); + + private MockWebServer server; + + @AfterEach + void cleanup() throws Exception { + if (this.server != null) { + this.server.shutdown(); + } + } + + @Test + public void parseWhenMetadataLocationConfiguredThenRequestMetadataFromLocation() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String serverUrl = this.server.url("/").toString(); + this.server.enqueue(xmlResponse(METADATA_RESPONSE)); + String metadataConfig = METADATA_LOCATION_XML_CONFIG.replace("${metadata-location}", serverUrl); + this.spring.context(metadataConfig).autowire(); + assertThat(this.relyingPartyRegistrationRepository) + .isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class); + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository + .findByRegistrationId("one"); + RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration + .getAssertingPartyDetails(); + assertThat(relyingPartyRegistration).isNotNull(); + assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one"); + assertThat(relyingPartyRegistration.getEntityId()) + .isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation()) + .isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(assertingPartyDetails.getEntityId()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"); + assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isFalse(); + assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getSingleSignOnServiceLocation()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php"); + assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(assertingPartyDetails.getSigningAlgorithms()) + .containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); + } + + @Test + public void parseWhenSingleRelyingPartyRegistrationThenAvailableInRepository() { + this.spring.configLocations(xml("SingleRegistration")).autowire(); + assertThat(this.relyingPartyRegistrationRepository) + .isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class); + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository + .findByRegistrationId("one"); + RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration + .getAssertingPartyDetails(); + assertThat(relyingPartyRegistration).isNotNull(); + assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one"); + assertThat(relyingPartyRegistration.getEntityId()) + .isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation()) + .isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()) + .isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(assertingPartyDetails.getEntityId()).isEqualTo("https://accounts.google.com/o/saml2/idp/entity-id"); + assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isTrue(); + assertThat(assertingPartyDetails.getSingleSignOnServiceLocation()) + .isEqualTo("https://accounts.google.com/o/saml2/idp/sso-url"); + assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getSigningAlgorithms()) + .containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); + } + + @Test + public void parseWhenMultiRelyingPartyRegistrationThenAvailableInRepository() { + this.spring.configLocations(xml("MultiRegistration")).autowire(); + assertThat(this.relyingPartyRegistrationRepository) + .isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class); + RelyingPartyRegistration one = this.relyingPartyRegistrationRepository.findByRegistrationId("one"); + RelyingPartyRegistration.AssertingPartyDetails google = one.getAssertingPartyDetails(); + RelyingPartyRegistration two = this.relyingPartyRegistrationRepository.findByRegistrationId("two"); + RelyingPartyRegistration.AssertingPartyDetails simpleSaml = two.getAssertingPartyDetails(); + assertThat(one).isNotNull(); + assertThat(one.getRegistrationId()).isEqualTo("one"); + assertThat(one.getEntityId()).isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(one.getAssertionConsumerServiceLocation()).isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(one.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(google.getEntityId()).isEqualTo("https://accounts.google.com/o/saml2/idp/entity-id"); + assertThat(google.getWantAuthnRequestsSigned()).isTrue(); + assertThat(google.getSingleSignOnServiceLocation()) + .isEqualTo("https://accounts.google.com/o/saml2/idp/sso-url"); + assertThat(google.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(google.getVerificationX509Credentials()).hasSize(1); + assertThat(google.getEncryptionX509Credentials()).hasSize(1); + assertThat(google.getSigningAlgorithms()).containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); + assertThat(two).isNotNull(); + assertThat(two.getRegistrationId()).isEqualTo("two"); + assertThat(two.getEntityId()).isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(two.getAssertionConsumerServiceLocation()).isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(two.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(simpleSaml.getEntityId()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"); + assertThat(simpleSaml.getWantAuthnRequestsSigned()).isFalse(); + assertThat(simpleSaml.getSingleSignOnServiceLocation()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php"); + assertThat(simpleSaml.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(simpleSaml.getVerificationX509Credentials()).hasSize(1); + assertThat(simpleSaml.getEncryptionX509Credentials()).hasSize(1); + assertThat(simpleSaml.getSigningAlgorithms()).containsExactly( + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224", + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"); + } + + private static MockResponse xmlResponse(String xml) { + return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).setBody(xml); + } + + private static String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index a3167f2d140..f4b85f45bad 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -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. @@ -30,6 +30,9 @@ import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy; @@ -48,6 +51,7 @@ * @author Rob Winch * @author Vedran Pavic * @author Ankur Pathak + * @author Marcus Da Coregio * @since 5.0 */ public class HeaderSpecTests { @@ -406,6 +410,53 @@ public void headersWhenCustomHeadersWriter() { assertHeaders(); } + @Test + public void headersWhenCrossOriginPoliciesCustomEnabledThenCustomCrossOriginPoliciesWritten() { + this.expectedHeaders.add(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY, + CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS + .getPolicy()); + this.expectedHeaders.add(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY, + CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP.getPolicy()); + this.expectedHeaders.add(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY, + CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN.getPolicy()); + // @formatter:off + this.http.headers() + .crossOriginOpenerPolicy() + .policy(CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS) + .and() + .crossOriginEmbedderPolicy() + .policy(CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + .and() + .crossOriginResourcePolicy() + .policy(CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + // @formatter:on + assertHeaders(); + } + + @Test + public void headersWhenCrossOriginPoliciesCustomEnabledInLambdaThenCustomCrossOriginPoliciesWritten() { + this.expectedHeaders.add(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY, + CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS + .getPolicy()); + this.expectedHeaders.add(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY, + CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP.getPolicy()); + this.expectedHeaders.add(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY, + CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN.getPolicy()); + // @formatter:off + this.http.headers() + .crossOriginOpenerPolicy((policy) -> policy + .policy(CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS) + ) + .crossOriginEmbedderPolicy((policy) -> policy + .policy(CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + ) + .crossOriginResourcePolicy((policy) -> policy + .policy(CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN) + ); + // @formatter:on + assertHeaders(); + } + private void expectHeaderNamesNotPresent(String... headerNames) { for (String headerName : headerNames) { this.expectedHeaders.remove(headerName); diff --git a/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java b/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java index 1c5eb64faa8..810de237afe 100644 --- a/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; import org.assertj.core.api.ThrowableAssert; import org.junit.jupiter.api.Test; @@ -33,6 +34,8 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.MethodParameter; import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.http.server.ServerHttpRequest; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -44,6 +47,8 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.expression.SecurityExpressionOperations; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; @@ -68,6 +73,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -168,6 +176,78 @@ public void sendWhenAnonymousMessageWithUnsubscribeMessageTypeThenPermitted() { send(message); } + @Test + public void sendWhenNoIdSpecifiedThenIntegratesWithAuthorizationManager() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + this.clientInboundChannel.send(message("/permitAll")); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> this.clientInboundChannel.send(message("/denyAll"))) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void sendWhenAnonymousMessageWithConnectMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), this.token.getToken()); + this.clientInboundChannel.send(message("/permitAll", headers)); + } + + @Test + public void sendWhenAnonymousMessageWithConnectAckMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.CONNECT_ACK); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithDisconnectMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.DISCONNECT); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithDisconnectAckMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.DISCONNECT_ACK); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithHeartbeatMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.HEARTBEAT); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithMessageMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.MESSAGE); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithOtherMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.OTHER); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithSubscribeMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.SUBSCRIBE); + send(message); + } + + @Test + public void sendWhenAnonymousMessageWithUnsubscribeMessageTypeThenAuthorizationManagerPermits() { + this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.UNSUBSCRIBE); + send(message); + } + @Test public void sendWhenConnectWithoutCsrfTokenThenDenied() { this.spring.configLocations(xml("SyncConfig")).autowire(); @@ -196,6 +276,19 @@ public void sendWhenInterceptWiredForMessageTypeThenDeniesOnTypeMismatch() { .withCauseInstanceOf(AccessDeniedException.class); } + @Test + public void sendWhenInterceptWiredForMessageTypeThenAuthorizationManagerDeniesOnTypeMismatch() { + this.spring.configLocations(xml("MessageInterceptTypeAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.MESSAGE); + send(message); + message = message("/permitAll", SimpMessageType.UNSUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + message = message("/anyOther", SimpMessageType.MESSAGE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + } + @Test public void sendWhenInterceptWiredForSubscribeTypeThenDeniesOnTypeMismatch() { this.spring.configLocations(xml("SubscribeInterceptTypeConfig")).autowire(); @@ -209,6 +302,19 @@ public void sendWhenInterceptWiredForSubscribeTypeThenDeniesOnTypeMismatch() { .withCauseInstanceOf(AccessDeniedException.class); } + @Test + public void sendWhenInterceptWiredForSubscribeTypeThenAuthorizationManagerDeniesOnTypeMismatch() { + this.spring.configLocations(xml("SubscribeInterceptTypeAuthorizationManager")).autowire(); + Message message = message("/permitAll", SimpMessageType.SUBSCRIBE); + send(message); + message = message("/permitAll", SimpMessageType.UNSUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + message = message("/anyOther", SimpMessageType.SUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + } + @Test public void configureWhenUsingConnectMessageTypeThenAutowireFails() { assertThatExceptionOfType(BeanDefinitionParsingException.class) @@ -309,6 +415,16 @@ public void sendWhenUsingCustomPathMatcherThenSecurityAppliesIt() { send(message); } + @Test + public void sendWhenUsingCustomPathMatcherThenAuthorizationManagerAppliesIt() { + this.spring.configLocations(xml("CustomPathMatcherAuthorizationManager")).autowire(); + Message message = message("/denyAll.a"); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + message = message("/denyAll.a.b"); + send(message); + } + @Test public void sendWhenIdSpecifiedThenSecurityDoesNotIntegrateWithClientInboundChannel() { this.spring.configLocations(xml("IdConfig")).autowire(); @@ -342,6 +458,27 @@ public void sendWhenCustomExpressionHandlerThenAuthorizesAccordingly() { .withCauseInstanceOf(AccessDeniedException.class); } + @Test + @WithMockUser(username = "nile") + public void sendWhenCustomExpressionHandlerThenAuthorizationManagerAuthorizesAccordingly() { + this.spring.configLocations(xml("CustomExpressionHandlerAuthorizationManager")).autowire(); + Message message = message("/denyNile"); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void sendWhenCustomAuthorizationManagerThenAuthorizesAccordingly() { + this.spring.configLocations(xml("CustomAuthorizationManagerConfig")).autowire(); + AuthorizationManager> authorizationManager = this.spring.getContext() + .getBean(AuthorizationManager.class); + given(authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + Message message = message("/any"); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + verify(authorizationManager).check(any(), any()); + } + private String xml(String configName) { return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; } @@ -466,6 +603,17 @@ public boolean denyNile() { }; } + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, + Message message) { + return new StandardEvaluationContext(new MessageSecurityExpressionRoot(authentication, message) { + public boolean denyNile() { + Authentication auth = getAuthentication(); + return auth != null && !"nile".equals(auth.getName()); + } + }); + } + } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt index fd840ebd936..870b9a92834 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt @@ -22,16 +22,16 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService -import org.springframework.security.core.userdetails.User import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.reactive.config.EnableWebFlux -import java.util.* +import java.util.Base64 /** * Tests for [AuthorizeExchangeDsl] @@ -181,4 +181,40 @@ class AuthorizeExchangeDslTests { return MapReactiveUserDetailsService(user) } } + + @Test + fun `request when ip address does not match then responds with forbidden`() { + this.spring.register(HasIpAddressConfig::class.java).autowire() + + this.client + .get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HasIpAddressConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasIpAddress("10.0.0.0/24")) + } + httpBasic { } + } + } + + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt index 3c404e28075..c68de3b4e7b 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt @@ -28,6 +28,9 @@ import org.springframework.security.config.test.SpringTestContextExtension import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter @@ -133,4 +136,60 @@ class ServerHeadersDslTests { } } } + + @Test + fun `request when no cross-origin policies configured then does not write cross-origin policies headers in response`() { + this.spring.register(CrossOriginPoliciesConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist("Cross-Origin-Opener-Policy") + .expectHeader().doesNotExist("Cross-Origin-Embedder-Policy") + .expectHeader().doesNotExist("Cross-Origin-Resource-Policy") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CrossOriginPoliciesConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { } + } + } + } + + @Test + fun `request when cross-origin custom policies configured then cross-origin custom policies headers in response`() { + this.spring.register(CrossOriginPoliciesCustomConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Cross-Origin-Opener-Policy", "same-origin") + .expectHeader().valueEquals("Cross-Origin-Embedder-Policy", "require-corp") + .expectHeader().valueEquals("Cross-Origin-Resource-Policy", "same-origin") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CrossOriginPoliciesCustomConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + crossOriginOpenerPolicy { + policy = CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN + } + crossOriginEmbedderPolicy { + policy = CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP + } + crossOriginResourcePolicy { + policy = CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN + } + } + } + } + } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDslTests.kt new file mode 100644 index 00000000000..95048a5a9d1 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeHttpRequestsDslTests.kt @@ -0,0 +1,797 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet + +import org.assertj.core.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.UnsatisfiedDependencyException +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod +import org.springframework.security.authorization.AuthorizationDecision +import org.springframework.security.authorization.AuthorizationManager +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.core.Authentication +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.access.intercept.RequestAuthorizationContext +import org.springframework.security.web.util.matcher.RegexRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.put +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.config.annotation.EnableWebMvc +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.util.WebUtils +import java.util.function.Supplier +import javax.servlet.DispatcherType + +/** + * Tests for [AuthorizeHttpRequestsDsl] + * + * @author Yuriy Savchenko + */ +@ExtendWith(SpringTestContextExtension::class) +class AuthorizeHttpRequestsDslTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `request when secured by regex matcher then responds with forbidden`() { + this.spring.register(AuthorizeHttpRequestsByRegexConfig::class.java).autowire() + + this.mockMvc.get("/private") + .andExpect { + status { isForbidden() } + } + } + + @Test + fun `request when allowed by regex matcher then responds with ok`() { + this.spring.register(AuthorizeHttpRequestsByRegexConfig::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isOk() } + } + } + + @Test + fun `request when allowed by regex matcher with http method then responds based on method`() { + this.spring.register(AuthorizeHttpRequestsByRegexConfig::class.java).autowire() + + this.mockMvc.post("/onlyPostPermitted") { with(csrf()) } + .andExpect { + status { isOk() } + } + + this.mockMvc.get("/onlyPostPermitted") + .andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + open class AuthorizeHttpRequestsByRegexConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(RegexRequestMatcher("/path", null), permitAll) + authorize(RegexRequestMatcher("/onlyPostPermitted", "POST"), permitAll) + authorize(RegexRequestMatcher("/onlyPostPermitted", "GET"), denyAll) + authorize(RegexRequestMatcher(".*", null), authenticated) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + + @RequestMapping("/onlyPostPermitted") + fun onlyPostPermitted() { + } + } + } + + @Test + fun `request when secured by mvc then responds with forbidden`() { + this.spring.register(AuthorizeHttpRequestsByMvcConfig::class.java).autowire() + + this.mockMvc.get("/private") + .andExpect { + status { isForbidden() } + } + } + + @Test + fun `request when allowed by mvc then responds with OK`() { + this.spring.register(AuthorizeHttpRequestsByMvcConfig::class.java, LegacyMvcMatchingConfig::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isOk() } + } + + this.mockMvc.get("/path.html") + .andExpect { + status { isOk() } + } + + this.mockMvc.get("/path/") + .andExpect { + status { isOk() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AuthorizeHttpRequestsByMvcConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/path", permitAll) + authorize("/**", authenticated) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Configuration + open class LegacyMvcMatchingConfig : WebMvcConfigurer { + override fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer.setUseSuffixPatternMatch(true) + } + } + + @Test + fun `request when secured by mvc path variables then responds based on path variable value`() { + this.spring.register(MvcMatcherPathVariablesConfig::class.java).autowire() + + this.mockMvc.get("/user/user") + .andExpect { + status { isOk() } + } + + this.mockMvc.get("/user/deny") + .andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherPathVariablesConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + val access = AuthorizationManager { _: Supplier, context: RequestAuthorizationContext -> + AuthorizationDecision(context.variables["userName"] == "user") + } + http { + authorizeHttpRequests { + authorize("/user/{userName}", access) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/user/{user}") + fun path(@PathVariable user: String) { + } + } + } + + @Test + fun `request when user has allowed role then responds with OK`() { + this.spring.register(HasRoleConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("admin", "password")) + }.andExpect { + status { isOk() } + } + } + + @Test + fun `request when user does not have allowed role then responds with forbidden`() { + this.spring.register(HasRoleConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("user", "password")) + }.andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class HasRoleConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/**", hasRole("ADMIN")) + } + httpBasic { } + } + return http.build() + } + + @RestController + internal class PathController { + @GetMapping("/") + fun index() { + } + } + + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + val adminDetails = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build() + return InMemoryUserDetailsManager(userDetails, adminDetails) + } + } + + @Test + fun `request when user has some allowed roles then responds with OK`() { + this.spring.register(HasAnyRoleConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("user", "password")) + }.andExpect { + status { isOk() } + } + + this.mockMvc.get("/") { + with(httpBasic("admin", "password")) + }.andExpect { + status { isOk() } + } + } + + @Test + fun `request when user does not have any allowed roles then responds with forbidden`() { + this.spring.register(HasAnyRoleConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("other", "password")) + }.andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class HasAnyRoleConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/**", hasAnyRole("ADMIN", "USER")) + } + httpBasic { } + } + return http.build() + } + + @RestController + internal class PathController { + @GetMapping("/") + fun index() { + } + } + + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + val admin1Details = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build() + val admin2Details = User.withDefaultPasswordEncoder() + .username("other") + .password("password") + .roles("OTHER") + .build() + return InMemoryUserDetailsManager(userDetails, admin1Details, admin2Details) + } + } + + @Test + fun `request when user has allowed authority then responds with OK`() { + this.spring.register(HasAuthorityConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("admin", "password")) + }.andExpect { + status { isOk() } + } + } + + @Test + fun `request when user does not have allowed authority then responds with forbidden`() { + this.spring.register(HasAuthorityConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("user", "password")) + }.andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class HasAuthorityConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/**", hasAuthority("ROLE_ADMIN")) + } + httpBasic { } + } + return http.build() + } + + @RestController + internal class PathController { + @GetMapping("/") + fun index() { + } + } + + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + val adminDetails = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build() + return InMemoryUserDetailsManager(userDetails, adminDetails) + } + } + + @Test + fun `request when user has some allowed authorities then responds with OK`() { + this.spring.register(HasAnyAuthorityConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("user", "password")) + }.andExpect { + status { isOk() } + } + + this.mockMvc.get("/") { + with(httpBasic("admin", "password")) + }.andExpect { + status { isOk() } + } + } + + @Test + fun `request when user does not have any allowed authorities then responds with forbidden`() { + this.spring.register(HasAnyAuthorityConfig::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("other", "password")) + }.andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class HasAnyAuthorityConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/**", hasAnyAuthority("ROLE_ADMIN", "ROLE_USER")) + } + httpBasic { } + } + return http.build() + } + + @RestController + internal class PathController { + @GetMapping("/") + fun index() { + } + } + + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("ROLE_USER") + .build() + val admin1Details = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .authorities("ROLE_ADMIN") + .build() + val admin2Details = User.withDefaultPasswordEncoder() + .username("other") + .password("password") + .authorities("ROLE_OTHER") + .build() + return InMemoryUserDetailsManager(userDetails, admin1Details, admin2Details) + } + } + + @Test + fun `request when secured by mvc with servlet path then responds based on servlet path`() { + this.spring.register(MvcMatcherServletPathConfig::class.java).autowire() + + this.mockMvc.perform(get("/spring/path") + .with { request -> + request.servletPath = "/spring" + request + }) + .andExpect(status().isForbidden) + + this.mockMvc.perform(get("/other/path") + .with { request -> + request.servletPath = "/other" + request + }) + .andExpect(status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherServletPathConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/path", "/spring", denyAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `request when secured by mvc with http method then responds based on http method`() { + this.spring.register(AuthorizeRequestsByMvcConfigWithHttpMethod::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isOk() } + } + + this.mockMvc.put("/path") { with(csrf()) } + .andExpect { + status { isForbidden() } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AuthorizeRequestsByMvcConfigWithHttpMethod { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(HttpMethod.GET, "/path", permitAll) + authorize(HttpMethod.PUT, "/path", denyAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `request when secured by mvc with servlet path and http method then responds based on path and method`() { + this.spring.register(MvcMatcherServletPathHttpMethodConfig::class.java).autowire() + + this.mockMvc.perform(get("/spring/path") + .with { request -> + request.apply { + servletPath = "/spring" + } + }) + .andExpect(status().isForbidden) + + this.mockMvc.perform(put("/spring/path") + .with { request -> + request.apply { + servletPath = "/spring" + csrf() + } + }) + .andExpect(status().isForbidden) + + this.mockMvc.perform(get("/other/path") + .with { request -> + request.apply { + servletPath = "/other" + } + }) + .andExpect(status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherServletPathHttpMethodConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(HttpMethod.GET, "/path", "/spring", denyAll) + authorize(HttpMethod.PUT, "/path", "/spring", denyAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `request when both authorizeRequests and authorizeHttpRequests configured then exception`() { + assertThatThrownBy { this.spring.register(BothAuthorizeRequestsConfig::class.java).autowire() } + .isInstanceOf(UnsatisfiedDependencyException::class.java) + .hasRootCauseInstanceOf(IllegalStateException::class.java) + .hasMessageContaining( + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one." + ) + } + + @EnableWebSecurity + @EnableWebMvc + open class BothAuthorizeRequestsConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize(anyRequest, permitAll) + } + authorizeHttpRequests { + authorize(anyRequest, denyAll) + } + } + return http.build() + } + } + + @Test + fun `request when shouldFilterAllDispatcherTypes and denyAll and ERROR then responds with forbidden`() { + this.spring.register(ShouldFilterAllDispatcherTypesTrueDenyAllConfig::class.java).autowire() + + this.mockMvc.perform(get("/path") + .with { request -> + request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error") + request.apply { + dispatcherType = DispatcherType.ERROR + } + }) + .andExpect(status().isForbidden) + } + + @EnableWebSecurity + @EnableWebMvc + open class ShouldFilterAllDispatcherTypesTrueDenyAllConfig { + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + shouldFilterAllDispatcherTypes = true + authorize(anyRequest, denyAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + + } + + @Test + fun `request when shouldFilterAllDispatcherTypes and permitAll and ERROR then responds with ok`() { + this.spring.register(ShouldFilterAllDispatcherTypesTruePermitAllConfig::class.java).autowire() + + this.mockMvc.perform(get("/path") + .with { request -> + request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error") + request.apply { + dispatcherType = DispatcherType.ERROR + } + }) + .andExpect(status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class ShouldFilterAllDispatcherTypesTruePermitAllConfig { + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + shouldFilterAllDispatcherTypes = true + authorize(anyRequest, permitAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + + } + + @Test + fun `request when shouldFilterAllDispatcherTypes false and ERROR dispatcher then responds with ok`() { + this.spring.register(ShouldFilterAllDispatcherTypesFalseAndDenyAllConfig::class.java).autowire() + + this.mockMvc.perform(get("/path") + .with { request -> + request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error") + request.apply { + dispatcherType = DispatcherType.ERROR + } + }) + .andExpect(status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class ShouldFilterAllDispatcherTypesFalseAndDenyAllConfig { + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + shouldFilterAllDispatcherTypes = false + authorize(anyRequest, denyAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + + } + + @Test + fun `request when shouldFilterAllDispatcherTypes omitted and ERROR dispatcher then responds with ok`() { + this.spring.register(ShouldFilterAllDispatcherTypesOmittedAndDenyAllConfig::class.java).autowire() + + this.mockMvc.perform(get("/path") + .with { request -> + request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error") + request.apply { + dispatcherType = DispatcherType.ERROR + } + }) + .andExpect(status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class ShouldFilterAllDispatcherTypesOmittedAndDenyAllConfig { + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, denyAll) + } + } + return http.build() + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt index c2cbfb371dd..b4e99646105 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt @@ -19,13 +19,13 @@ package org.springframework.security.config.web.servlet import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean import org.springframework.http.HttpHeaders import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension -import org.springframework.security.config.web.servlet.headers.PermissionsPolicyDsl import org.springframework.security.web.header.writers.StaticHeadersWriter import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt index eea2c3bff1e..1e981ac57f5 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -53,6 +53,9 @@ import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.web.servlet.config.annotation.EnableWebMvc import javax.servlet.Filter +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.security.web.SecurityFilterChain /** * Tests for [HttpSecurityDsl] @@ -128,9 +131,13 @@ class HttpSecurityDslTests { } } - @Test - fun `request when it does not match the security request matcher then the security rules do not apply`() { - this.spring.register(SecurityRequestMatcherConfig::class.java).autowire() + @ParameterizedTest + @ValueSource(classes = [ + SecurityRequestMatcherRequestsConfig::class, + SecurityRequestMatcherHttpRequestsConfig::class + ]) + fun `request when it does not match the security request matcher then the security rules do not apply`(config: Class<*>) { + this.spring.register(config).autowire() this.mockMvc.get("/") .andExpect { @@ -138,9 +145,13 @@ class HttpSecurityDslTests { } } - @Test - fun `request when it matches the security request matcher then the security rules apply`() { - this.spring.register(SecurityRequestMatcherConfig::class.java).autowire() + @ParameterizedTest + @ValueSource(classes = [ + SecurityRequestMatcherRequestsConfig::class, + SecurityRequestMatcherHttpRequestsConfig::class + ]) + fun `request when it matches the security request matcher then the security rules apply`(config: Class<*>) { + this.spring.register(config).autowire() this.mockMvc.get("/path") .andExpect { @@ -149,7 +160,7 @@ class HttpSecurityDslTests { } @EnableWebSecurity - open class SecurityRequestMatcherConfig : WebSecurityConfigurerAdapter() { + open class SecurityRequestMatcherRequestsConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { securityMatcher(RegexRequestMatcher("/path", null)) @@ -160,9 +171,27 @@ class HttpSecurityDslTests { } } - @Test - fun `request when it does not match the security pattern matcher then the security rules do not apply`() { - this.spring.register(SecurityPatternMatcherConfig::class.java).autowire() + @EnableWebSecurity + open class SecurityRequestMatcherHttpRequestsConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher(RegexRequestMatcher("/path", null)) + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + } + + @ParameterizedTest + @ValueSource(classes = [ + SecurityPatternMatcherRequestsConfig::class, + SecurityPatternMatcherHttpRequestsConfig::class + ]) + fun `request when it does not match the security pattern matcher then the security rules do not apply`(config: Class<*>) { + this.spring.register(config).autowire() this.mockMvc.get("/") .andExpect { @@ -170,9 +199,13 @@ class HttpSecurityDslTests { } } - @Test - fun `request when it matches the security pattern matcher then the security rules apply`() { - this.spring.register(SecurityPatternMatcherConfig::class.java).autowire() + @ParameterizedTest + @ValueSource(classes = [ + SecurityPatternMatcherRequestsConfig::class, + SecurityPatternMatcherHttpRequestsConfig::class + ]) + fun `request when it matches the security pattern matcher then the security rules apply`(config: Class<*>) { + this.spring.register(config).autowire() this.mockMvc.get("/path") .andExpect { @@ -182,7 +215,7 @@ class HttpSecurityDslTests { @EnableWebSecurity @EnableWebMvc - open class SecurityPatternMatcherConfig : WebSecurityConfigurerAdapter() { + open class SecurityPatternMatcherRequestsConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { securityMatcher("/path") @@ -193,9 +226,28 @@ class HttpSecurityDslTests { } } - @Test - fun `security pattern matcher when used with security request matcher then both apply`() { - this.spring.register(MultiMatcherConfig::class.java).autowire() + @EnableWebSecurity + @EnableWebMvc + open class SecurityPatternMatcherHttpRequestsConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher("/path") + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + } + + @ParameterizedTest + @ValueSource(classes = [ + MultiMatcherRequestsConfig::class, + MultiMatcherHttpRequestsConfig::class + ]) + fun `security pattern matcher when used with security request matcher then both apply`(config: Class<*>) { + this.spring.register(config).autowire() this.mockMvc.get("/path1") .andExpect { @@ -215,7 +267,7 @@ class HttpSecurityDslTests { @EnableWebSecurity @EnableWebMvc - open class MultiMatcherConfig : WebSecurityConfigurerAdapter() { + open class MultiMatcherRequestsConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { securityMatcher("/path1") @@ -227,28 +279,48 @@ class HttpSecurityDslTests { } } - @Test - fun `authentication manager when configured in DSL then used`() { - this.spring.register(AuthenticationManagerConfig::class.java).autowire() + @EnableWebSecurity + @EnableWebMvc + open class MultiMatcherHttpRequestsConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher("/path1") + securityMatcher(RegexRequestMatcher("/path2", null)) + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + } + + @ParameterizedTest + @ValueSource(classes = [ + AuthenticationManagerRequestsConfig::class, + AuthenticationManagerHttpRequestsConfig::class + ]) + fun `authentication manager when configured in DSL then used`(config: Class<*>) { + this.spring.register(config).autowire() mockkObject(AuthenticationManagerConfig.AUTHENTICATION_MANAGER) every { AuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any()) } returns TestingAuthenticationToken("user", "test", "ROLE_USER") - val request = MockMvcRequestBuilders.get("/") + val request = MockMvcRequestBuilders.get("/") .with(httpBasic("user", "password")) this.mockMvc.perform(request) verify(exactly = 1) { AuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any()) } } - @EnableWebSecurity - open class AuthenticationManagerConfig : WebSecurityConfigurerAdapter() { - companion object { - val AUTHENTICATION_MANAGER: AuthenticationManager = ProviderManager(TestingAuthenticationProvider()) - } + object AuthenticationManagerConfig { + val AUTHENTICATION_MANAGER: AuthenticationManager = ProviderManager(TestingAuthenticationProvider()) + } + @EnableWebSecurity + open class AuthenticationManagerRequestsConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { - authenticationManager = AUTHENTICATION_MANAGER + authenticationManager = AuthenticationManagerConfig.AUTHENTICATION_MANAGER authorizeRequests { authorize(anyRequest, authenticated) } @@ -257,6 +329,21 @@ class HttpSecurityDslTests { } } + @EnableWebSecurity + open class AuthenticationManagerHttpRequestsConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authenticationManager = AuthenticationManagerConfig.AUTHENTICATION_MANAGER + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + httpBasic { } + } + return http.build() + } + } + @Test fun `HTTP security when custom filter configured then custom filter added to filter chain`() { this.spring.register(CustomFilterConfig::class.java).autowire() diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/SecurityContextDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/SecurityContextDslTests.kt new file mode 100644 index 00000000000..630dd637f8e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/SecurityContextDslTests.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.ObjectPostProcessor +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.userdetails.PasswordEncodedUser +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.context.* +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + +@ExtendWith(SpringTestContextExtension::class) +class SecurityContextDslTests { + + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mvc: MockMvc + + @Test + fun `configure when registering object post processor then invoked on security context persistence filter`() { + spring.register(ObjectPostProcessorConfig::class.java).autowire() + verify { ObjectPostProcessorConfig.objectPostProcessor.postProcess(any()) } + } + + @EnableWebSecurity + open class ObjectPostProcessorConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + securityContext { } + } + // @formatter:on + } + + @Bean + open fun objectPostProcessor(): ObjectPostProcessor = objectPostProcessor + + companion object { + var objectPostProcessor: ObjectPostProcessor = spyk(ReflectingObjectPostProcessor()) + + class ReflectingObjectPostProcessor : ObjectPostProcessor { + override fun postProcess(`object`: O): O = `object` + } + } + } + + @Test + fun `security context when invoked twice then uses original security context repository`() { + spring.register(DuplicateDoesNotOverrideConfig::class.java).autowire() + every { DuplicateDoesNotOverrideConfig.SECURITY_CONTEXT_REPOSITORY.loadContext(any()) } returns mockk(relaxed = true) + mvc.perform(get("/")) + verify(exactly = 1) { DuplicateDoesNotOverrideConfig.SECURITY_CONTEXT_REPOSITORY.loadContext(any()) } + } + + + @EnableWebSecurity + open class DuplicateDoesNotOverrideConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + securityContext { + securityContextRepository = SECURITY_CONTEXT_REPOSITORY + } + securityContext { } + } + // @formatter:on + } + + companion object { + var SECURITY_CONTEXT_REPOSITORY = mockk(relaxed = true) + } + } + + @Test + fun `security context when security context repository not configured then does not throw exception`() { + spring.register(SecurityContextRepositoryDefaultsSecurityContextRepositoryConfig::class.java).autowire() + assertDoesNotThrow { mvc.perform(get("/")) } + } + + @EnableWebSecurity + open class SecurityContextRepositoryDefaultsSecurityContextRepositoryConfig : WebSecurityConfigurerAdapter(true) { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + addFilterAt(WebAsyncManagerIntegrationFilter()) + anonymous { } + securityContext { } + authorizeRequests { + authorize(anyRequest, permitAll) + } + httpBasic { } + } + // @formatter:on + } + + override fun configure(auth: AuthenticationManagerBuilder) { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + // @formatter:on + } + } + + @Test + fun `security context when require explicit save is true then configure SecurityContextHolderFilter`() { + val repository = HttpSessionSecurityContextRepository() + val testContext = spring.register(RequireExplicitSaveConfig::class.java) + testContext.autowire() + val filterChainProxy = testContext.context.getBean(FilterChainProxy::class.java) + // @formatter:off + val filterTypes = filterChainProxy.getFilters("/").toList() + + assertThat(filterTypes) + .anyMatch { it is SecurityContextHolderFilter } + .noneMatch { it is SecurityContextPersistenceFilter } + // @formatter:on + val mvcResult = mvc.perform(SecurityMockMvcRequestBuilders.formLogin()).andReturn() + val securityContext = repository + .loadContext(HttpRequestResponseHolder(mvcResult.request, mvcResult.response)) + assertThat(securityContext.authentication).isNotNull + } + + @EnableWebSecurity + open class RequireExplicitSaveConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + // @formatter:off + http { + formLogin { } + securityContext { + requireExplicitSave = true + } + } + // @formatter:on + } + + override fun configure(auth: AuthenticationManagerBuilder) { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()) + // @formatter:on + } + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml new file mode 100644 index 00000000000..981d7b6a8c8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-MinimalAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-MinimalAuthorizationManager.xml new file mode 100644 index 00000000000..293fbe8e96f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-MinimalAuthorizationManager.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml new file mode 100644 index 00000000000..cfa473c0d55 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml new file mode 100644 index 00000000000..1e688e556be --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml new file mode 100644 index 00000000000..d667ebc5e95 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml new file mode 100644 index 00000000000..667933f8d6b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPathAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPathAuthorizationManager.xml new file mode 100644 index 00000000000..856892c7196 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPathAuthorizationManager.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariablesAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariablesAuthorizationManager.xml new file mode 100644 index 00000000000..598394585bc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariablesAuthorizationManager.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPathAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPathAuthorizationManager.xml new file mode 100644 index 00000000000..25fab705793 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPathAuthorizationManager.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPathAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPathAuthorizationManager.xml new file mode 100644 index 00000000000..3fb406032fd --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPathAuthorizationManager.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRoleAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRoleAuthorizationManager.xml new file mode 100644 index 00000000000..3215253b71e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRoleAuthorizationManager.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersAuthorizationManager.xml new file mode 100644 index 00000000000..5d1b053a660 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersAuthorizationManager.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariablesAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariablesAuthorizationManager.xml new file mode 100644 index 00000000000..60fc53b7cd8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariablesAuthorizationManager.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPathAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPathAuthorizationManager.xml new file mode 100644 index 00000000000..866017eeb28 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPathAuthorizationManager.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethodAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethodAuthorizationManager.xml new file mode 100644 index 00000000000..d525d6f2b61 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethodAuthorizationManager.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariablesAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariablesAuthorizationManager.xml new file mode 100644 index 00000000000..43cdb2bbf3b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariablesAuthorizationManager.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPathAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPathAuthorizationManager.xml new file mode 100644 index 00000000000..0d0690efd76 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPathAuthorizationManager.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256AuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256AuthorizationManager.xml new file mode 100644 index 00000000000..a01eba3055c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256AuthorizationManager.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariablesAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariablesAuthorizationManager.xml new file mode 100644 index 00000000000..3ca186ee725 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariablesAuthorizationManager.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-DisableUrlRewriting-NullSecurityContextRepository.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-DisableUrlRewriting-NullSecurityContextRepository.xml new file mode 100644 index 00000000000..f3eae6a3a3c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-DisableUrlRewriting-NullSecurityContextRepository.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml new file mode 100644 index 00000000000..381fff8dca7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml new file mode 100644 index 00000000000..4406065ff68 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml new file mode 100644 index 00000000000..45349131c78 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml new file mode 100644 index 00000000000..fcd568996fc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml new file mode 100644 index 00000000000..d694c40b12f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml new file mode 100644 index 00000000000..687f2700b0c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml new file mode 100644 index 00000000000..64b1e70b6c9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml new file mode 100644 index 00000000000..2c8c8d620c4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml new file mode 100644 index 00000000000..af5de241c3f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml new file mode 100644 index 00000000000..806a6fdb528 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml new file mode 100644 index 00000000000..91e270febc0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml new file mode 100644 index 00000000000..5fc49103dc1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml new file mode 100644 index 00000000000..7caa59eb922 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml new file mode 100644 index 00000000000..66068ed58d6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml new file mode 100644 index 00000000000..79ad59b7a6f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml new file mode 100644 index 00000000000..9c4b1c6497a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml new file mode 100644 index 00000000000..dd274db7eb2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml new file mode 100644 index 00000000000..2b97e6f1c13 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml b/config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml new file mode 100644 index 00000000000..2e4b358083c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml b/config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml new file mode 100644 index 00000000000..445d1e5d64f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt b/config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt new file mode 100644 index 00000000000..9c4ee078e22 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD +VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX +c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw +aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa +BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD +DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr +QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62 +E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz +2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW +RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ +nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5 +cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph +iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5 +ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO +nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v +ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu +xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z +V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3 +lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk +-----END CERTIFICATE----- diff --git a/config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml b/config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml new file mode 100644 index 00000000000..c96d06ac7cc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt b/config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt new file mode 100644 index 00000000000..b907e2fffde --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC +VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG +A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD +DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1 +MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES +MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN +TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos +vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM ++U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG +y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi +XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+ +qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD +RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B +-----END CERTIFICATE----- diff --git a/config/src/test/resources/org/springframework/security/config/saml2/rp-private.key b/config/src/test/resources/org/springframework/security/config/saml2/rp-private.key new file mode 100644 index 00000000000..73196e020c5 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/rp-private.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE +VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK +cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6 +Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn +x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5 +wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd +vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY +8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX +oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx +EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0 +KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt +YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr +9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM +INrtuLp4YHbgk1mi +-----END PRIVATE KEY----- diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml new file mode 100644 index 00000000000..5827be1b756 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomExpressionHandlerAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomExpressionHandlerAuthorizationManager.xml new file mode 100644 index 00000000000..926b5b1120e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomExpressionHandlerAuthorizationManager.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomPathMatcherAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomPathMatcherAuthorizationManager.xml new file mode 100644 index 00000000000..c4cd2563afd --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomPathMatcherAuthorizationManager.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-MessageInterceptTypeAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-MessageInterceptTypeAuthorizationManager.xml new file mode 100644 index 00000000000..e636e41185b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-MessageInterceptTypeAuthorizationManager.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-NoIdAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-NoIdAuthorizationManager.xml new file mode 100644 index 00000000000..b7553797609 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-NoIdAuthorizationManager.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypeAuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypeAuthorizationManager.xml new file mode 100644 index 00000000000..d7f62dd0323 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypeAuthorizationManager.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 213f24f92c5..3f135f759fe 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -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' diff --git a/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java index 6a70491d7ca..8da167f306c 100644 --- a/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -35,6 +35,7 @@ * objects. * * @author Luke Taylor + * @author Evgeniy Cheban * @since 3.1 */ public abstract class AbstractSecurityExpressionHandler @@ -116,6 +117,10 @@ public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } + protected BeanResolver getBeanResolver() { + return this.beanResolver; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.beanResolver = new BeanFactoryResolver(applicationContext); diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionHandler.java index 39e171c4dda..6a596347143 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.access.expression; +import java.util.function.Supplier; + import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.expression.EvaluationContext; import org.springframework.expression.ExpressionParser; @@ -26,6 +28,7 @@ * expressions from the implementation of the underlying expression objects * * @author Luke Taylor + * @author Evgeniy Cheban * @since 3.1 */ public interface SecurityExpressionHandler extends AopInfrastructureBean { @@ -41,4 +44,19 @@ public interface SecurityExpressionHandler extends AopInfrastructureBean { */ EvaluationContext createEvaluationContext(Authentication authentication, T invocation); + /** + * Provides an evaluation context in which to evaluate security expressions for the + * invocation type. You can override this method in order to provide a custom + * implementation that uses lazy initialization of the {@link Authentication} object. + * By default, this method uses eager initialization of the {@link Authentication} + * object. + * @param authentication the {@link Supplier} of the {@link Authentication} to use + * @param invocation the {@link T} to use + * @return the {@link EvaluationContext} to use + * @since 5.8 + */ + default EvaluationContext createEvaluationContext(Supplier authentication, T invocation) { + return createEvaluationContext(authentication.get(), invocation); + } + } diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java index 155b317638b..de27a7d063c 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.Collection; import java.util.Set; +import java.util.function.Supplier; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; @@ -26,16 +27,18 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.util.Assert; /** * Base root object for use in Spring Security expression evaluations. * * @author Luke Taylor + * @author Evgeniy Cheban * @since 3.0 */ public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { - protected final Authentication authentication; + private final Supplier authentication; private AuthenticationTrustResolver trustResolver; @@ -72,10 +75,18 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat * @param authentication the {@link Authentication} to use. Cannot be null. */ public SecurityExpressionRoot(Authentication authentication) { - if (authentication == null) { - throw new IllegalArgumentException("Authentication object cannot be null"); - } - this.authentication = authentication; + this(() -> authentication); + } + + /** + * Creates a new instance that uses lazy initialization of the {@link Authentication} + * object. + * @param authentication the {@link Supplier} of the {@link Authentication} to use. + * Cannot be null. + * @since 5.8 + */ + public SecurityExpressionRoot(Supplier authentication) { + this.authentication = new AuthenticationSupplier(authentication); } @Override @@ -111,7 +122,7 @@ private boolean hasAnyAuthorityName(String prefix, String... roles) { @Override public final Authentication getAuthentication() { - return this.authentication; + return this.authentication.get(); } @Override @@ -126,7 +137,7 @@ public final boolean denyAll() { @Override public final boolean isAnonymous() { - return this.trustResolver.isAnonymous(this.authentication); + return this.trustResolver.isAnonymous(getAuthentication()); } @Override @@ -136,13 +147,13 @@ public final boolean isAuthenticated() { @Override public final boolean isRememberMe() { - return this.trustResolver.isRememberMe(this.authentication); + return this.trustResolver.isRememberMe(getAuthentication()); } @Override public final boolean isFullyAuthenticated() { - return !this.trustResolver.isAnonymous(this.authentication) - && !this.trustResolver.isRememberMe(this.authentication); + Authentication authentication = getAuthentication(); + return !this.trustResolver.isAnonymous(authentication) && !this.trustResolver.isRememberMe(authentication); } /** @@ -151,7 +162,7 @@ public final boolean isFullyAuthenticated() { * @return */ public Object getPrincipal() { - return this.authentication.getPrincipal(); + return getAuthentication().getPrincipal(); } public void setTrustResolver(AuthenticationTrustResolver trustResolver) { @@ -181,7 +192,7 @@ public void setDefaultRolePrefix(String defaultRolePrefix) { private Set getAuthoritySet() { if (this.roles == null) { - Collection userAuthorities = this.authentication.getAuthorities(); + Collection userAuthorities = getAuthentication().getAuthorities(); if (this.roleHierarchy != null) { userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities); } @@ -192,12 +203,12 @@ private Set getAuthoritySet() { @Override public boolean hasPermission(Object target, Object permission) { - return this.permissionEvaluator.hasPermission(this.authentication, target, permission); + return this.permissionEvaluator.hasPermission(getAuthentication(), target, permission); } @Override public boolean hasPermission(Object targetId, String targetType, Object permission) { - return this.permissionEvaluator.hasPermission(this.authentication, (Serializable) targetId, targetType, + return this.permissionEvaluator.hasPermission(getAuthentication(), (Serializable) targetId, targetType, permission); } @@ -225,4 +236,27 @@ private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String return defaultRolePrefix + role; } + private static final class AuthenticationSupplier implements Supplier { + + private Authentication value; + + private final Supplier delegate; + + private AuthenticationSupplier(Supplier delegate) { + Assert.notNull(delegate, "delegate cannot be null"); + this.delegate = delegate; + } + + @Override + public Authentication get() { + if (this.value == null) { + Authentication authentication = this.delegate.get(); + Assert.notNull(authentication, "Authentication object cannot be null"); + this.value = authentication; + } + return this.value; + } + + } + } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java index 6254ee087d7..1c76e2b47ab 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java @@ -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. @@ -23,6 +23,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import java.util.stream.Stream; import org.aopalliance.intercept.MethodInvocation; @@ -50,6 +51,7 @@ * support. * * @author Luke Taylor + * @author Evgeniy Cheban * @since 3.0 */ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler @@ -77,12 +79,26 @@ public StandardEvaluationContext createEvaluationContextInternal(Authentication return new MethodSecurityEvaluationContext(auth, mi, getParameterNameDiscoverer()); } + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, MethodInvocation mi) { + MethodSecurityExpressionOperations root = createSecurityExpressionRoot(authentication, mi); + MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(root, mi, + getParameterNameDiscoverer()); + ctx.setBeanResolver(getBeanResolver()); + return ctx; + } + /** * Creates the root object for expression evaluation. */ @Override protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) { + return createSecurityExpressionRoot(() -> authentication, invocation); + } + + private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier authentication, + MethodInvocation invocation) { MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication); root.setThis(invocation.getThis()); root.setPermissionEvaluator(getPermissionEvaluator()); diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java index 5ba0a376b67..b3413565516 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java @@ -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. @@ -34,6 +34,7 @@ * * @author Luke Taylor * @author Daniel Bustamante + * @author Evgeniy Cheban * @since 3.0 */ class MethodSecurityEvaluationContext extends MethodBasedEvaluationContext { @@ -52,6 +53,11 @@ class MethodSecurityEvaluationContext extends MethodBasedEvaluationContext { super(mi.getThis(), getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer); } + MethodSecurityEvaluationContext(MethodSecurityExpressionOperations root, MethodInvocation mi, + ParameterNameDiscoverer parameterNameDiscoverer) { + super(root, getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer); + } + private static Method getSpecificMethod(MethodInvocation mi) { return AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(mi.getThis())); } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java index a4d3f0a0151..5e17069a53a 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.access.expression.method; +import java.util.function.Supplier; + import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.core.Authentication; @@ -23,6 +25,7 @@ * Extended expression root object which contains extra method-specific functionality. * * @author Luke Taylor + * @author Evgeniy Cheban * @since 3.0 */ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations { @@ -37,6 +40,10 @@ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements Met super(a); } + MethodSecurityExpressionRoot(Supplier authentication) { + super(authentication); + } + @Override public void setFilterObject(Object filterObject) { this.filterObject = filterObject; diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java index d75a2775cd1..5a08efd019c 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java @@ -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. @@ -117,7 +117,7 @@ private Mono upgradeEncodingIfNecessary(UserDetails userDetails, St } private UsernamePasswordAuthenticationToken createUsernamePasswordAuthenticationToken(UserDetails userDetails) { - return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), + return UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); } diff --git a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java index 55963150a60..be796d04a4c 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java @@ -32,6 +32,7 @@ * String. * * @author Ben Alex + * @author Norbert Nowak */ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { @@ -71,6 +72,33 @@ public UsernamePasswordAuthenticationToken(Object principal, Object credentials, super.setAuthenticated(true); // must use super, as we override } + /** + * This factory method can be safely used by any code that wishes to create a + * unauthenticated UsernamePasswordAuthenticationToken. + * @param principal + * @param credentials + * @return UsernamePasswordAuthenticationToken with false isAuthenticated() result + * + * @since 5.7 + */ + public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials); + } + + /** + * This factory method can be safely used by any code that wishes to create a + * authenticated UsernamePasswordAuthenticationToken. + * @param principal + * @param credentials + * @return UsernamePasswordAuthenticationToken with true isAuthenticated() result + * + * @since 5.7 + */ + public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials, + Collection authorities) { + return new UsernamePasswordAuthenticationToken(principal, credentials, authorities); + } + @Override public Object getCredentials() { return this.credentials; diff --git a/core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java index f85306cdd31..7d5b434d523 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java @@ -193,7 +193,7 @@ protected Authentication createSuccessAuthentication(Object principal, Authentic // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details - UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, + UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); this.logger.debug("Authenticated user"); diff --git a/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationManagerImpl.java b/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationManagerImpl.java index 1d32b90ee5c..731ce15f6d0 100644 --- a/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationManagerImpl.java +++ b/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationManagerImpl.java @@ -47,7 +47,8 @@ public void afterPropertiesSet() { @Override public Collection attemptAuthentication(String username, String password) throws RemoteAuthenticationException { - UsernamePasswordAuthenticationToken request = new UsernamePasswordAuthenticationToken(username, password); + UsernamePasswordAuthenticationToken request = UsernamePasswordAuthenticationToken.unauthenticated(username, + password); try { return this.authenticationManager.authenticate(request).getAuthorities(); } diff --git a/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProvider.java index c7164a0b97c..a617b3b60fa 100644 --- a/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProvider.java @@ -68,7 +68,7 @@ public Authentication authenticate(Authentication authentication) throws Authent String password = (credentials != null) ? credentials.toString() : null; Collection authorities = this.remoteAuthenticationManager .attemptAuthentication(username, password); - return new UsernamePasswordAuthenticationToken(username, password, authorities); + return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities); } public RemoteAuthenticationManager getRemoteAuthenticationManager() { diff --git a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java index baa65ac041a..ec76b6f7ce5 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java @@ -41,6 +41,19 @@ public final class AuthenticatedAuthorizationManager implements Authorization public static AuthenticatedAuthorizationManager authenticated() { return new AuthenticatedAuthorizationManager<>(); } + + public static AuthenticatedAuthorizationManager fullyAuthenticated() { + + } + + public static AuthenticatedAuthorizationManager anonymous() { + + } + + public static AuthenticatedAuthorizationManager rememberMe() { + + } + /** * Determines if the current user is authorized by evaluating if the diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java index 8cfc0dcf0ae..ed9c072e577 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.security.authorization; -import java.util.HashSet; +import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.function.Supplier; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; @@ -37,10 +40,23 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana private static final String ROLE_PREFIX = "ROLE_"; - private final Set authorities; + private final List authorities; + + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); private AuthorityAuthorizationManager(String... authorities) { - this.authorities = new HashSet<>(AuthorityUtils.createAuthorityList(authorities)); + this.authorities = AuthorityUtils.createAuthorityList(authorities); + } + + /** + * Sets the {@link RoleHierarchy} to be used. Default is {@link NullRoleHierarchy}. + * Cannot be null. + * @param roleHierarchy the {@link RoleHierarchy} to use + * @since 5.8 + */ + public void setRoleHierarchy(RoleHierarchy roleHierarchy) { + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; } /** @@ -106,6 +122,18 @@ public static AuthorityAuthorizationManager hasAnyAuthority(String... aut return new AuthorityAuthorizationManager<>(authorities); } + public static AuthorityAuthorizationManager RememberMe() { + + } + + public static AuthorityAuthorizationManager fullyAuthenticated(String... ) { + + } + + public static AuthorityAuthorizationManager anonymous(String... ) { + + } + private static String[] toNamedRolesArray(String rolePrefix, String[] roles) { String[] result = new String[roles.length]; for (int i = 0; i < roles.length; i++) { @@ -132,14 +160,19 @@ private boolean isGranted(Authentication authentication) { } private boolean isAuthorized(Authentication authentication) { - for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { - if (this.authorities.contains(grantedAuthority)) { + Set authorities = AuthorityUtils.authorityListToSet(this.authorities); + for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) { + if (authorities.contains(grantedAuthority.getAuthority())) { return true; } } return false; } + private Collection getGrantedAuthorities(Authentication authentication) { + return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); + } + @Override public String toString() { return "AuthorityAuthorizationManager[authorities=" + this.authorities + "]"; diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java index 5c98cf3061a..6a91cfb8938 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java @@ -45,9 +45,10 @@ public class AuthorityReactiveAuthorizationManager implements ReactiveAuthori @Override public Mono check(Mono 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 diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java new file mode 100644 index 00000000000..de972c59b22 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.function.Supplier; + +import org.springframework.security.authorization.event.AuthorizationDeniedEvent; +import org.springframework.security.authorization.event.AuthorizationGrantedEvent; +import org.springframework.security.core.Authentication; + +/** + * A contract for publishing authorization events + * + * @author Parikshit Dutta + * @author Josh Cummings + * @since 5.7 + * @see AuthorizationManager + */ +public interface AuthorizationEventPublisher { + + /** + * Publish the given details in the form of an event, typically + * {@link AuthorizationGrantedEvent} or {@link AuthorizationDeniedEvent}. + * + * Note that success events can be very noisy if enabled by default. Because of this + * implementations may choose to drop success events by default. + * @param authentication a {@link Supplier} for the current user + * @param object the secured object + * @param decision the decision about whether the user may access the secured object + * @param the secured object's type + */ + void publishAuthorizationEvent(Supplier authentication, T object, + AuthorizationDecision decision); + +} diff --git a/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java b/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java new file mode 100644 index 00000000000..2ef21a28730 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.authorization.event.AuthorizationDeniedEvent; +import org.springframework.security.authorization.event.AuthorizationGrantedEvent; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An implementation of {@link AuthorizationEventPublisher} that uses Spring's event + * publishing support. + * + * Because {@link AuthorizationGrantedEvent}s typically require additional business logic + * to decide whether to publish, this implementation only publishes + * {@link AuthorizationDeniedEvent}s. + * + * @author Parikshit Dutta + * @author Josh Cummings + * @since 5.7 + */ +public final class SpringAuthorizationEventPublisher implements AuthorizationEventPublisher { + + private final ApplicationEventPublisher eventPublisher; + + /** + * Construct this publisher using Spring's {@link ApplicationEventPublisher} + * @param eventPublisher + */ + public SpringAuthorizationEventPublisher(ApplicationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "eventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + } + + /** + * {@inheritDoc} + */ + @Override + public void publishAuthorizationEvent(Supplier authentication, T object, + AuthorizationDecision decision) { + if (decision == null || decision.isGranted()) { + return; + } + AuthorizationDeniedEvent failure = new AuthorizationDeniedEvent<>(authentication, object, decision); + this.eventPublisher.publishEvent(failure); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/event/AuthorizationDeniedEvent.java b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationDeniedEvent.java new file mode 100644 index 00000000000..0b514fd32f2 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationDeniedEvent.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.event; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationEvent; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; + +/** + * An {@link ApplicationEvent} which indicates failed authorization. + * + * @author Parikshit Dutta + * @author Josh Cummings + * @since 5.7 + */ +public class AuthorizationDeniedEvent extends ApplicationEvent { + + private final Supplier authentication; + + private final AuthorizationDecision decision; + + public AuthorizationDeniedEvent(Supplier authentication, T object, AuthorizationDecision decision) { + super(object); + this.authentication = authentication; + this.decision = decision; + } + + public Supplier getAuthentication() { + return this.authentication; + } + + public AuthorizationDecision getAuthorizationDecision() { + return this.decision; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/event/AuthorizationGrantedEvent.java b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationGrantedEvent.java new file mode 100644 index 00000000000..4e210214e19 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationGrantedEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.event; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationEvent; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link ApplicationEvent} which indicates successful authorization. + * + * @author Parikshit Dutta + * @author Josh Cummings + * @since 5.7 + */ +public class AuthorizationGrantedEvent extends ApplicationEvent { + + private final Supplier authentication; + + private final AuthorizationDecision decision; + + public AuthorizationGrantedEvent(Supplier authentication, T object, + AuthorizationDecision decision) { + super(object); + Assert.notNull(authentication, "authentication supplier cannot be null"); + this.authentication = authentication; + this.decision = decision; + } + + public Supplier getAuthentication() { + return this.authentication; + } + + public AuthorizationDecision getAuthorizationDecision() { + return this.decision; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java index d40c51c1f6b..5a2b4fba7f0 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java @@ -22,7 +22,6 @@ import org.aopalliance.intercept.MethodInvocation; -import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.NonNull; import org.springframework.security.authorization.AuthorizationManager; @@ -46,7 +45,7 @@ abstract class AbstractAuthorizationManagerRegistry { final AuthorizationManager getManager(MethodInvocation methodInvocation) { Method method = methodInvocation.getMethod(); Object target = methodInvocation.getThis(); - Class targetClass = (target != null) ? AopUtils.getTargetClass(target) : null; + Class targetClass = (target != null) ? target.getClass() : null; MethodClassKey cacheKey = new MethodClassKey(method, targetClass); return this.cachedManagers.computeIfAbsent(cacheKey, (k) -> resolveManager(method, targetClass)); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java index 17defe9cde8..42b7cd92c03 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java @@ -22,7 +22,6 @@ import org.aopalliance.intercept.MethodInvocation; -import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.NonNull; @@ -43,7 +42,7 @@ abstract class AbstractExpressionAttributeRegistry targetClass = (target != null) ? AopUtils.getTargetClass(target) : null; + Class targetClass = (target != null) ? target.getClass() : null; return getAttribute(method, targetClass); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java index e93777aaa0a..91662e07e4e 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -33,6 +33,7 @@ import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -66,6 +67,8 @@ public final class AuthorizationManagerAfterMethodInterceptor private int order; + private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish; + /** * Creates an instance. * @param pointcut the {@link Pointcut} to use @@ -122,6 +125,17 @@ public void setOrder(int order) { this.order = order; } + /** + * Use this {@link AuthorizationEventPublisher} to publish the + * {@link AuthorizationManager} result. + * @param eventPublisher + * @since 5.7 + */ + public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "eventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + } + /** * {@inheritDoc} */ @@ -142,8 +156,9 @@ public boolean isPerInstance() { private void attemptAuthorization(MethodInvocation mi, Object result) { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); - AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, - new MethodInvocationResult(mi, result)); + MethodInvocationResult object = new MethodInvocationResult(mi, result); + AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, object); + this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, object, decision); if (decision != null && !decision.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision)); @@ -152,4 +167,9 @@ private void attemptAuthorization(MethodInvocation mi, Object result) { this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi)); } + private static void noPublish(Supplier authentication, T object, + AuthorizationDecision decision) { + + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java index 38b6f03b1bf..808b2a9c988 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -38,6 +38,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -71,6 +72,8 @@ public final class AuthorizationManagerBeforeMethodInterceptor private int order = AuthorizationInterceptorsOrder.FIRST.getOrder(); + private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish; + /** * Creates an instance. * @param pointcut the {@link Pointcut} to use @@ -168,6 +171,17 @@ public void setOrder(int order) { this.order = order; } + /** + * Use this {@link AuthorizationEventPublisher} to publish the + * {@link AuthorizationManager} result. + * @param eventPublisher + * @since 5.7 + */ + public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "eventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + } + /** * {@inheritDoc} */ @@ -189,6 +203,7 @@ public boolean isPerInstance() { private void attemptAuthorization(MethodInvocation mi) { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, mi); + this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, mi, decision); if (decision != null && !decision.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision)); @@ -197,4 +212,9 @@ private void attemptAuthorization(MethodInvocation mi) { this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi)); } + private static void noPublish(Supplier authentication, T object, + AuthorizationDecision decision) { + + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java index 7d594e37054..3d605f08531 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -72,7 +72,7 @@ public AuthorizationDecision check(Supplier authentication, Meth if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return null; } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation()); this.expressionHandler.setReturnObject(mi.getResult(), ctx); boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java index 737d7aa8e26..10794fde8cb 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -128,7 +128,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return returnedObject; } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(AUTHENTICATION_SUPPLIER.get(), mi); + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(AUTHENTICATION_SUPPLIER, mi); return this.expressionHandler.filter(returnedObject, attribute.getExpression(), ctx); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java index b3d844cc759..cbf861a2bb3 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -72,7 +72,7 @@ public AuthorizationDecision check(Supplier authentication, Meth if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return null; } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), mi); + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi); boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); return new ExpressionAttributeAuthorizationDecision(granted, attribute); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java index d9cfc29dee5..148be0a3b59 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -126,7 +126,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { if (attribute == PreFilterExpressionAttribute.NULL_ATTRIBUTE) { return mi.proceed(); } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(AUTHENTICATION_SUPPLIER.get(), mi); + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(AUTHENTICATION_SUPPLIER, mi); Object filterTarget = findFilterTarget(attribute.filterTarget, ctx, mi); this.expressionHandler.filter(filterTarget, attribute.getExpression(), ctx); return mi.proceed(); diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index 2b13e626773..df54a6d6f16 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -43,7 +43,7 @@ public final class SpringSecurityCoreVersion { * N.B. Classes are not intended to be serializable between different versions. See * SEC-1709 for why we still need a serial version. */ - public static final long SERIAL_VERSION_UID = 560L; + public static final long SERIAL_VERSION_UID = 580L; static final String MIN_SPRING_VERSION = getSpringVersion(); diff --git a/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java index 3c5f763feff..1ab3ac9e634 100644 --- a/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -62,6 +62,32 @@ public final class ListeningSecurityContextHolderStrategy implements SecurityCon private final SecurityContextHolderStrategy delegate; + /** + * Construct a {@link ListeningSecurityContextHolderStrategy} based on + * {@link ThreadLocalSecurityContextHolderStrategy} + * @param listeners the listeners that should be notified when the + * {@link SecurityContext} is {@link #setContext(SecurityContext) set} or + * {@link #clearContext() cleared} + * + * @since 5.7 + */ + public ListeningSecurityContextHolderStrategy(Collection listeners) { + this(new ThreadLocalSecurityContextHolderStrategy(), listeners); + } + + /** + * Construct a {@link ListeningSecurityContextHolderStrategy} based on + * {@link ThreadLocalSecurityContextHolderStrategy} + * @param listeners the listeners that should be notified when the + * {@link SecurityContext} is {@link #setContext(SecurityContext) set} or + * {@link #clearContext() cleared} + * + * @since 5.7 + */ + public ListeningSecurityContextHolderStrategy(SecurityContextChangedListener... listeners) { + this(new ThreadLocalSecurityContextHolderStrategy(), listeners); + } + /** * Construct a {@link ListeningSecurityContextHolderStrategy} * @param listeners the listeners that should be notified when the diff --git a/core/src/main/java/org/springframework/security/core/context/TransientSecurityContext.java b/core/src/main/java/org/springframework/security/core/context/TransientSecurityContext.java new file mode 100644 index 00000000000..0089ae455d0 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/context/TransientSecurityContext.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core.context; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.Transient; + +/** + * A {@link SecurityContext} that is annotated with @{@link Transient} and thus should + * never be stored across requests. This is useful in situations where one might run as a + * different user for part of a request. + * + * @author Rob Winch + * @since 5.7 + */ +@Transient +public class TransientSecurityContext extends SecurityContextImpl { + + public TransientSecurityContext() { + } + + public TransientSecurityContext(Authentication authentication) { + super(authentication); + } + +} diff --git a/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java b/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java index 347d820a996..47dcf9b4a91 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java @@ -110,6 +110,8 @@ */ public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware { + public static final String DEFAULT_USER_SCHEMA_DDL_LOCATION = "org/springframework/security/core/userdetails/jdbc/users.ddl"; + // @formatter:off public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled " + "from users " diff --git a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java index 1aa8eb0213f..3a91f0c1369 100644 --- a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java +++ b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java @@ -64,6 +64,8 @@ public void setupModule(SetupContext context) { UnmodifiableSetMixin.class); context.setMixInAnnotations(Collections.unmodifiableList(Collections.emptyList()).getClass(), UnmodifiableListMixin.class); + context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(), + UnmodifiableMapMixin.class); context.setMixInAnnotations(User.class, UserMixin.class); context.setMixInAnnotations(UsernamePasswordAuthenticationToken.class, UsernamePasswordAuthenticationTokenMixin.class); diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index febd2b755cc..d5eaf6e2933 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-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. @@ -63,6 +63,7 @@ * mapper.registerModule(new WebServletJackson2Module()); * mapper.registerModule(new WebServerJackson2Module()); * mapper.registerModule(new OAuth2ClientJackson2Module()); + * mapper.registerModule(new Saml2Jackson2Module()); * * * @author Jitendra Singh. @@ -84,6 +85,10 @@ public final class SecurityJackson2Modules { private static final String javaTimeJackson2ModuleClass = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"; + private static final String ldapJackson2ModuleClass = "org.springframework.security.ldap.jackson2.LdapJackson2Module"; + + private static final String saml2Jackson2ModuleClass = "org.springframework.security.saml2.jackson2.Saml2Jackson2Module"; + private SecurityJackson2Modules() { } @@ -129,6 +134,12 @@ public static List getModules(ClassLoader loader) { if (ClassUtils.isPresent(javaTimeJackson2ModuleClass, loader)) { addToModulesList(loader, modules, javaTimeJackson2ModuleClass); } + if (ClassUtils.isPresent(ldapJackson2ModuleClass, loader)) { + addToModulesList(loader, modules, ldapJackson2ModuleClass); + } + if (ClassUtils.isPresent(saml2Jackson2ModuleClass, loader)) { + addToModulesList(loader, modules, saml2Jackson2ModuleClass); + } return modules; } @@ -204,6 +215,7 @@ static class AllowlistTypeIdResolver implements TypeIdResolver { names.add("java.util.HashMap"); names.add("java.util.LinkedHashMap"); names.add("org.springframework.security.core.context.SecurityContextImpl"); + names.add("java.util.Arrays$ArrayList"); ALLOWLIST_CLASS_NAMES = Collections.unmodifiableSet(names); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java similarity index 65% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java rename to core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java index d44b9c72782..a9345658604 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapDeserializer.java +++ b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java @@ -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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.oauth2.client.jackson2; +package org.springframework.security.jackson2; import java.io.IOException; import java.util.Collections; @@ -28,22 +28,22 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** - * A {@code JsonDeserializer} for {@link Collections#unmodifiableMap(Map)}. + * Custom deserializer for {@link UnmodifiableMapMixin}. * - * @author Joe Grandja - * @since 5.3 - * @see Collections#unmodifiableMap(Map) + * @author Ulrich Grave + * @since 5.7 * @see UnmodifiableMapMixin */ -final class UnmodifiableMapDeserializer extends JsonDeserializer> { +class UnmodifiableMapDeserializer extends JsonDeserializer> { @Override - public Map deserialize(JsonParser parser, DeserializationContext context) throws IOException { - ObjectMapper mapper = (ObjectMapper) parser.getCodec(); - JsonNode mapNode = mapper.readTree(parser); + public Map deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode node = mapper.readTree(jp); + Map result = new LinkedHashMap<>(); - if (mapNode != null && mapNode.isObject()) { - Iterable> fields = mapNode::fields; + if (node != null && node.isObject()) { + Iterable> fields = node::fields; for (Map.Entry field : fields) { result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java similarity index 65% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java rename to core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java index 95941cc7e02..802c6d19644 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/UnmodifiableMapMixin.java +++ b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java @@ -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. @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.springframework.security.oauth2.client.jackson2; +package org.springframework.security.jackson2; -import java.util.Collections; import java.util.Map; import com.fasterxml.jackson.annotation.JsonCreator; @@ -24,19 +23,23 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; /** - * This mixin class is used to serialize/deserialize - * {@link Collections#unmodifiableMap(Map)}. It also registers a custom deserializer - * {@link UnmodifiableMapDeserializer}. + * This mixin class used to deserialize java.util.Collections$UnmodifiableMap and used + * with various AuthenticationToken implementation's mixin classes. * - * @author Joe Grandja - * @since 5.3 - * @see Collections#unmodifiableMap(Map) + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new CoreJackson2Module());
      + * 
      + * + * @author Ulrich Grave + * @since 5.7 * @see UnmodifiableMapDeserializer - * @see OAuth2ClientJackson2Module + * @see CoreJackson2Module + * @see SecurityJackson2Modules */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) @JsonDeserialize(using = UnmodifiableMapDeserializer.class) -abstract class UnmodifiableMapMixin { +class UnmodifiableMapMixin { @JsonCreator UnmodifiableMapMixin(Map map) { diff --git a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java index c5d815ad790..aebdf3c827e 100644 --- a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java +++ b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-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. @@ -78,8 +78,8 @@ public UsernamePasswordAuthenticationToken deserialize(JsonParser jp, Deserializ List authorities = mapper.readValue(readJsonNode(jsonNode, "authorities").traverse(mapper), GRANTED_AUTHORITY_LIST); UsernamePasswordAuthenticationToken token = (!authenticated) - ? new UsernamePasswordAuthenticationToken(principal, credentials) - : new UsernamePasswordAuthenticationToken(principal, credentials, authorities); + ? UsernamePasswordAuthenticationToken.unauthenticated(principal, credentials) + : UsernamePasswordAuthenticationToken.authenticated(principal, credentials, authorities); JsonNode detailsNode = readJsonNode(jsonNode, "details"); if (detailsNode.isNull() || detailsNode.isMissingNode()) { token.setDetails(null); diff --git a/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java index c613035a05a..8e7b6b254d5 100644 --- a/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -125,7 +125,8 @@ public void changePassword(String oldPassword, String newPassword) { // supplied password. if (this.authenticationManager != null) { this.logger.debug(LogMessage.format("Reauthenticating user '%s' for password change request.", username)); - this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword)); + this.authenticationManager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated(username, oldPassword)); } else { this.logger.debug("No authentication manager set. Password won't be re-checked."); diff --git a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java index 2cfda0ab06e..264568f49f1 100644 --- a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -271,7 +271,8 @@ public void changePassword(String oldPassword, String newPassword) throws Authen // supplied password. if (this.authenticationManager != null) { this.logger.debug(LogMessage.format("Reauthenticating user '%s' for password change request.", username)); - this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword)); + this.authenticationManager + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated(username, oldPassword)); } else { this.logger.debug("No authentication manager set. Password won't be re-checked."); @@ -287,8 +288,8 @@ public void changePassword(String oldPassword, String newPassword) throws Authen protected Authentication createNewAuthentication(Authentication currentAuth, String newPassword) { UserDetails user = loadUserByUsername(currentAuth.getName()); - UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(user, null, - user.getAuthorities()); + UsernamePasswordAuthenticationToken newAuthentication = UsernamePasswordAuthenticationToken.authenticated(user, + null, user.getAuthorities()); newAuthentication.setDetails(currentAuth.getDetails()); return newAuthentication; } diff --git a/core/src/test/java/org/springframework/security/access/AuthorizationFailureEventTests.java b/core/src/test/java/org/springframework/security/access/AuthorizationFailureEventTests.java index fde3f5e7004..cd1c569907f 100644 --- a/core/src/test/java/org/springframework/security/access/AuthorizationFailureEventTests.java +++ b/core/src/test/java/org/springframework/security/access/AuthorizationFailureEventTests.java @@ -34,7 +34,8 @@ */ public class AuthorizationFailureEventTests { - private final UsernamePasswordAuthenticationToken foo = new UsernamePasswordAuthenticationToken("foo", "bar"); + private final UsernamePasswordAuthenticationToken foo = UsernamePasswordAuthenticationToken.unauthenticated("foo", + "bar"); private List attributes = SecurityConfig.createList("TEST"); diff --git a/core/src/test/java/org/springframework/security/access/AuthorizedEventTests.java b/core/src/test/java/org/springframework/security/access/AuthorizedEventTests.java index b55a060eb88..c5655ec2822 100644 --- a/core/src/test/java/org/springframework/security/access/AuthorizedEventTests.java +++ b/core/src/test/java/org/springframework/security/access/AuthorizedEventTests.java @@ -34,13 +34,13 @@ public class AuthorizedEventTests { @Test public void testRejectsNulls() { assertThatIllegalArgumentException().isThrownBy(() -> new AuthorizedEvent(null, - SecurityConfig.createList("TEST"), new UsernamePasswordAuthenticationToken("foo", "bar"))); + SecurityConfig.createList("TEST"), UsernamePasswordAuthenticationToken.unauthenticated("foo", "bar"))); } @Test public void testRejectsNulls2() { assertThatIllegalArgumentException().isThrownBy(() -> new AuthorizedEvent(new SimpleMethodInvocation(), null, - new UsernamePasswordAuthenticationToken("foo", "bar"))); + UsernamePasswordAuthenticationToken.unauthenticated("foo", "bar"))); } @Test diff --git a/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java b/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java index ce5d2b015e6..33c4a77face 100644 --- a/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java +++ b/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java @@ -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. @@ -19,10 +19,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import org.aopalliance.intercept.MethodInvocation; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,6 +34,8 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -43,6 +47,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; @ExtendWith(MockitoExtension.class) public class DefaultMethodSecurityExpressionHandlerTests { @@ -167,6 +172,20 @@ public void filterStreamWhenClosedThenUpstreamGetsClosed() { verify(upstream).close(); } + @Test + public void createEvaluationContextSupplierAuthentication() { + setupMocks(); + Supplier mockAuthenticationSupplier = mock(Supplier.class); + given(mockAuthenticationSupplier.get()).willReturn(this.authentication); + EvaluationContext context = this.handler.createEvaluationContext(mockAuthenticationSupplier, + this.methodInvocation); + verifyNoInteractions(mockAuthenticationSupplier); + assertThat(context.getRootObject()).extracting(TypedValue::getValue) + .asInstanceOf(InstanceOfAssertFactories.type(MethodSecurityExpressionRoot.class)) + .extracting(SecurityExpressionRoot::getAuthentication).isEqualTo(this.authentication); + verify(mockAuthenticationSupplier).get(); + } + static class Foo { void bar() { diff --git a/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java b/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java index 41eddcd218d..cd877b09cd4 100644 --- a/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java +++ b/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java @@ -44,8 +44,8 @@ public void testAlwaysSupportsClass() { @Test public void testDoesNotReturnAdditionalAuthoritiesIfCalledWithoutARunAsSetting() { - UsernamePasswordAuthenticationToken inputToken = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + UsernamePasswordAuthenticationToken inputToken = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); RunAsManagerImpl runAs = new RunAsManagerImpl(); runAs.setKey("my_password"); Authentication resultingToken = runAs.buildRunAs(inputToken, new Object(), @@ -55,8 +55,8 @@ public void testDoesNotReturnAdditionalAuthoritiesIfCalledWithoutARunAsSetting() @Test public void testRespectsRolePrefix() { - UsernamePasswordAuthenticationToken inputToken = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ONE", "TWO")); + UsernamePasswordAuthenticationToken inputToken = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ONE", "TWO")); RunAsManagerImpl runAs = new RunAsManagerImpl(); runAs.setKey("my_password"); runAs.setRolePrefix("FOOBAR_"); @@ -75,8 +75,8 @@ public void testRespectsRolePrefix() { @Test public void testReturnsAdditionalGrantedAuthorities() { - UsernamePasswordAuthenticationToken inputToken = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + UsernamePasswordAuthenticationToken inputToken = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); RunAsManagerImpl runAs = new RunAsManagerImpl(); runAs.setKey("my_password"); Authentication result = runAs.buildRunAs(inputToken, new Object(), diff --git a/core/src/test/java/org/springframework/security/access/vote/AuthenticatedVoterTests.java b/core/src/test/java/org/springframework/security/access/vote/AuthenticatedVoterTests.java index 1d2cfa3657f..bff472e3623 100644 --- a/core/src/test/java/org/springframework/security/access/vote/AuthenticatedVoterTests.java +++ b/core/src/test/java/org/springframework/security/access/vote/AuthenticatedVoterTests.java @@ -44,7 +44,7 @@ private Authentication createAnonymous() { } private Authentication createFullyAuthenticated() { - return new UsernamePasswordAuthenticationToken("ignored", "ignored", + return UsernamePasswordAuthenticationToken.authenticated("ignored", "ignored", AuthorityUtils.createAuthorityList("ignored")); } diff --git a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java index e0a1fe336c4..b64dbce4fee 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -66,12 +66,13 @@ public Object getPrincipal() { @Test public void credentialsAreClearedByDefault() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("Test", + "Password"); ProviderManager mgr = makeProviderManager(); Authentication result = mgr.authenticate(token); assertThat(result.getCredentials()).isNull(); mgr.setEraseCredentialsAfterAuthentication(false); - token = new UsernamePasswordAuthenticationToken("Test", "Password"); + token = UsernamePasswordAuthenticationToken.unauthenticated("Test", "Password"); result = mgr.authenticate(token); assertThat(result.getCredentials()).isNotNull(); } diff --git a/core/src/test/java/org/springframework/security/authentication/ReactiveUserDetailsServiceAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ReactiveUserDetailsServiceAuthenticationManagerTests.java index eabd9256c52..cca23a0dae5 100644 --- a/core/src/test/java/org/springframework/security/authentication/ReactiveUserDetailsServiceAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ReactiveUserDetailsServiceAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -72,7 +72,7 @@ public void constructorNullUserDetailsService() { @Test public void authenticateWhenUserNotFoundThenBadCredentials() { given(this.repository.findByUsername(this.username)).willReturn(Mono.empty()); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.username, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.username, this.password); Mono authentication = this.manager.authenticate(token); // @formatter:off @@ -91,7 +91,7 @@ public void authenticateWhenPasswordNotEqualThenBadCredentials() { .build(); // @formatter:on given(this.repository.findByUsername(user.getUsername())).willReturn(Mono.just(user)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.username, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.username, this.password + "INVALID"); Mono authentication = this.manager.authenticate(token); // @formatter:off @@ -110,7 +110,7 @@ public void authenticateWhenSuccessThenSuccess() { .build(); // @formatter:on given(this.repository.findByUsername(user.getUsername())).willReturn(Mono.just(user)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.username, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.username, this.password); Authentication authentication = this.manager.authenticate(token).block(); assertThat(authentication).isEqualTo(authentication); @@ -122,7 +122,7 @@ public void authenticateWhenPasswordEncoderAndSuccessThenSuccess() { given(this.passwordEncoder.matches(any(), any())).willReturn(true); User user = new User(this.username, this.password, AuthorityUtils.createAuthorityList("ROLE_USER")); given(this.repository.findByUsername(user.getUsername())).willReturn(Mono.just(user)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.username, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.username, this.password); Authentication authentication = this.manager.authenticate(token).block(); assertThat(authentication).isEqualTo(authentication); @@ -134,7 +134,7 @@ public void authenticateWhenPasswordEncoderAndFailThenFail() { given(this.passwordEncoder.matches(any(), any())).willReturn(false); User user = new User(this.username, this.password, AuthorityUtils.createAuthorityList("ROLE_USER")); given(this.repository.findByUsername(user.getUsername())).willReturn(Mono.just(user)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.username, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.username, this.password); Mono authentication = this.manager.authenticate(token); // @formatter:off diff --git a/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java b/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java index 0583c42a488..cdeb4ba1d82 100644 --- a/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java +++ b/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -35,7 +35,7 @@ public static Authentication authenticatedUser() { } public static Authentication autheticated(UserDetails user) { - return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + return UsernamePasswordAuthenticationToken.authenticated(user, null, user.getAuthorities()); } } diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java index 50e73593d26..c8f39d52e38 100644 --- a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -95,7 +95,7 @@ public void authenticateWhenCustomSchedulerThenUsed() { given(this.encoder.matches(any(), any())).willReturn(true); this.manager.setScheduler(this.scheduler); this.manager.setPasswordEncoder(this.encoder); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.user, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.user, this.user.getPassword()); Authentication result = this.manager.authenticate(token).block(); verify(this.scheduler).schedule(any()); @@ -111,7 +111,7 @@ public void authenticateWhenPasswordServiceThenUpdated() { given(this.userDetailsPasswordService.updatePassword(any(), any())).willReturn(Mono.just(this.user)); this.manager.setPasswordEncoder(this.encoder); this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.user, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.user, this.user.getPassword()); Authentication result = this.manager.authenticate(token).block(); verify(this.encoder).encode(this.user.getPassword()); @@ -124,7 +124,7 @@ public void authenticateWhenPasswordServiceAndBadCredentialsThenNotUpdated() { given(this.encoder.matches(any(), any())).willReturn(false); this.manager.setPasswordEncoder(this.encoder); this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.user, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.user, this.user.getPassword()); assertThatExceptionOfType(BadCredentialsException.class) .isThrownBy(() -> this.manager.authenticate(token).block()); @@ -138,7 +138,7 @@ public void authenticateWhenPasswordServiceAndUpgradeFalseThenNotUpdated() { given(this.encoder.upgradeEncoding(any())).willReturn(false); this.manager.setPasswordEncoder(this.encoder); this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.user, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.user, this.user.getPassword()); Authentication result = this.manager.authenticate(token).block(); verifyZeroInteractions(this.userDetailsPasswordService); @@ -152,8 +152,8 @@ public void authenticateWhenPostAuthenticationChecksFail() { this.manager.setPasswordEncoder(this.encoder); this.manager.setPostAuthenticationChecks(this.postAuthenticationChecks); assertThatExceptionOfType(LockedException.class).isThrownBy(() -> this.manager - .authenticate(new UsernamePasswordAuthenticationToken(this.user, this.user.getPassword())).block()) - .withMessage("account is locked"); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated(this.user, this.user.getPassword())) + .block()).withMessage("account is locked"); verify(this.postAuthenticationChecks).check(eq(this.user)); } @@ -162,7 +162,7 @@ public void authenticateWhenPostAuthenticationChecksNotSet() { given(this.userDetailsService.findByUsername(any())).willReturn(Mono.just(this.user)); given(this.encoder.matches(any(), any())).willReturn(true); this.manager.setPasswordEncoder(this.encoder); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(this.user, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(this.user, this.user.getPassword()); this.manager.authenticate(token).block(); verifyZeroInteractions(this.postAuthenticationChecks); @@ -179,7 +179,7 @@ public void authenticateWhenAccountExpiredThenException() { .build(); // @formatter:on given(this.userDetailsService.findByUsername(any())).willReturn(Mono.just(expiredUser)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(expiredUser, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(expiredUser, expiredUser.getPassword()); assertThatExceptionOfType(AccountExpiredException.class) .isThrownBy(() -> this.manager.authenticate(token).block()); @@ -196,7 +196,7 @@ public void authenticateWhenAccountLockedThenException() { .build(); // @formatter:on given(this.userDetailsService.findByUsername(any())).willReturn(Mono.just(lockedUser)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(lockedUser, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(lockedUser, lockedUser.getPassword()); assertThatExceptionOfType(LockedException.class).isThrownBy(() -> this.manager.authenticate(token).block()); } @@ -212,7 +212,7 @@ public void authenticateWhenAccountDisabledThenException() { .build(); // @formatter:on given(this.userDetailsService.findByUsername(any())).willReturn(Mono.just(disabledUser)); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(disabledUser, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(disabledUser, disabledUser.getPassword()); assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> this.manager.authenticate(token).block()); } diff --git a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java index 8c3eda3dd8a..4f9e38e7666 100644 --- a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java @@ -33,8 +33,8 @@ public class UsernamePasswordAuthenticationTokenTests { @Test public void authenticatedPropertyContractIsSatisfied() { - UsernamePasswordAuthenticationToken grantedToken = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.NO_AUTHORITIES); + UsernamePasswordAuthenticationToken grantedToken = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.NO_AUTHORITIES); // check default given we passed some GrantedAuthorty[]s (well, we passed empty // list) assertThat(grantedToken.isAuthenticated()).isTrue(); @@ -44,8 +44,8 @@ public void authenticatedPropertyContractIsSatisfied() { assertThat(!grantedToken.isAuthenticated()).isTrue(); // Now let's create a UsernamePasswordAuthenticationToken without any // GrantedAuthorty[]s (different constructor) - UsernamePasswordAuthenticationToken noneGrantedToken = new UsernamePasswordAuthenticationToken("Test", - "Password"); + UsernamePasswordAuthenticationToken noneGrantedToken = UsernamePasswordAuthenticationToken + .unauthenticated("Test", "Password"); assertThat(!noneGrantedToken.isAuthenticated()).isTrue(); // check we're allowed to still set it to untrusted noneGrantedToken.setAuthenticated(false); @@ -56,8 +56,8 @@ public void authenticatedPropertyContractIsSatisfied() { @Test public void gettersReturnCorrectData() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); assertThat(token.getPrincipal()).isEqualTo("Test"); assertThat(token.getCredentials()).isEqualTo("Password"); assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_ONE"); @@ -71,4 +71,18 @@ public void testNoArgConstructorDoesntExist() throws Exception { .isThrownBy(() -> clazz.getDeclaredConstructor((Class[]) null)); } + @Test + public void unauthenticatedFactoryMethodResultsUnauthenticatedToken() { + UsernamePasswordAuthenticationToken grantedToken = UsernamePasswordAuthenticationToken.unauthenticated("Test", + "Password"); + assertThat(grantedToken.isAuthenticated()).isFalse(); + } + + @Test + public void authenticatedFactoryMethodResultsAuthenticatedToken() { + UsernamePasswordAuthenticationToken grantedToken = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", AuthorityUtils.NO_AUTHORITIES); + assertThat(grantedToken.isAuthenticated()).isTrue(); + } + } diff --git a/core/src/test/java/org/springframework/security/authentication/anonymous/AnonymousAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/anonymous/AnonymousAuthenticationTokenTests.java index 4910b622d51..780d391418b 100644 --- a/core/src/test/java/org/springframework/security/authentication/anonymous/AnonymousAuthenticationTokenTests.java +++ b/core/src/test/java/org/springframework/security/authentication/anonymous/AnonymousAuthenticationTokenTests.java @@ -81,8 +81,8 @@ public void testNotEqualsDueToAbstractParentEqualsCheck() { @Test public void testNotEqualsDueToDifferentAuthenticationClass() { AnonymousAuthenticationToken token1 = new AnonymousAuthenticationToken("key", "Test", ROLES_12); - UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test", "Password", - ROLES_12); + UsernamePasswordAuthenticationToken token2 = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", ROLES_12); assertThat(token1.equals(token2)).isFalse(); } diff --git a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java index 1771721b7f0..0eb2488b5c6 100644 --- a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java @@ -74,7 +74,7 @@ public class DaoAuthenticationProviderTests { @Test public void testAuthenticateFailsForIncorrectPasswordCase() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "KOala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "KOala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); @@ -87,14 +87,16 @@ public void testReceivedBadCredentialsWhenCredentialsNotProvided() { DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("rod", null); + UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken + .unauthenticated("rod", null); assertThatExceptionOfType(BadCredentialsException.class) .isThrownBy(() -> provider.authenticate(authenticationToken)); } @Test public void testAuthenticateFailsIfAccountExpired() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("peter", + "opal"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserPeterAccountExpired()); provider.setUserCache(new MockUserCache()); @@ -103,7 +105,8 @@ public void testAuthenticateFailsIfAccountExpired() { @Test public void testAuthenticateFailsIfAccountLocked() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("peter", + "opal"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserPeterAccountLocked()); provider.setUserCache(new MockUserCache()); @@ -115,17 +118,18 @@ public void testAuthenticateFailsIfCredentialsExpired() { DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserPeterCredentialsExpired()); provider.setUserCache(new MockUserCache()); - assertThatExceptionOfType(CredentialsExpiredException.class) - .isThrownBy(() -> provider.authenticate(new UsernamePasswordAuthenticationToken("peter", "opal"))); + assertThatExceptionOfType(CredentialsExpiredException.class).isThrownBy( + () -> provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("peter", "opal"))); // Check that wrong password causes BadCredentialsException, rather than // CredentialsExpiredException - assertThatExceptionOfType(BadCredentialsException.class).isThrownBy( - () -> provider.authenticate(new UsernamePasswordAuthenticationToken("peter", "wrong_password"))); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> provider + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("peter", "wrong_password"))); } @Test public void testAuthenticateFailsIfUserDisabled() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("peter", + "opal"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserPeter()); provider.setUserCache(new MockUserCache()); @@ -134,7 +138,7 @@ public void testAuthenticateFailsIfUserDisabled() { @Test public void testAuthenticateFailsWhenAuthenticationDaoHasBackendFailure() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceSimulateBackendError()); provider.setUserCache(new MockUserCache()); @@ -144,7 +148,7 @@ public void testAuthenticateFailsWhenAuthenticationDaoHasBackendFailure() { @Test public void testAuthenticateFailsWithEmptyUsername() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(null, "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); @@ -153,7 +157,8 @@ public void testAuthenticateFailsWithEmptyUsername() { @Test public void testAuthenticateFailsWithInvalidPassword() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "INVALID_PASSWORD"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", + "INVALID_PASSWORD"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); @@ -162,7 +167,8 @@ public void testAuthenticateFailsWithInvalidPassword() { @Test public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionFalse() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("INVALID_USER", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("INVALID_USER", + "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setHideUserNotFoundExceptions(false); // we want // UsernameNotFoundExceptions @@ -173,7 +179,8 @@ public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundException @Test public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionsWithDefaultOfTrue() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("INVALID_USER", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("INVALID_USER", + "koala"); DaoAuthenticationProvider provider = createProvider(); assertThat(provider.isHideUserNotFoundExceptions()).isTrue(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); @@ -183,7 +190,8 @@ public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundException @Test public void testAuthenticateFailsWithInvalidUsernameAndChangePasswordEncoder() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("INVALID_USER", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("INVALID_USER", + "koala"); DaoAuthenticationProvider provider = createProvider(); assertThat(provider.isHideUserNotFoundExceptions()).isTrue(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); @@ -195,7 +203,7 @@ public void testAuthenticateFailsWithInvalidUsernameAndChangePasswordEncoder() { @Test public void testAuthenticateFailsWithMixedCaseUsernameIfDefaultChanged() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("RoD", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("RoD", "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); @@ -204,7 +212,7 @@ public void testAuthenticateFailsWithMixedCaseUsernameIfDefaultChanged() { @Test public void testAuthenticates() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); token.setDetails("192.168.0.1"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); @@ -222,7 +230,7 @@ public void testAuthenticates() { @Test public void testAuthenticatesASecondTime() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); @@ -240,7 +248,7 @@ public void testAuthenticatesASecondTime() { @Test public void testAuthenticatesWithForcePrincipalAsString() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); provider.setUserCache(new MockUserCache()); @@ -258,7 +266,8 @@ public void testAuthenticatesWithForcePrincipalAsString() { public void authenticateWhenSuccessAndPasswordManagerThenUpdates() { String password = "password"; String encodedPassword = "encoded"; - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", password); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + password); PasswordEncoder encoder = mock(PasswordEncoder.class); UserDetailsService userDetailsService = mock(UserDetailsService.class); UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); @@ -279,7 +288,8 @@ public void authenticateWhenSuccessAndPasswordManagerThenUpdates() { @Test public void authenticateWhenBadCredentialsAndPasswordManagerThenNoUpdate() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); PasswordEncoder encoder = mock(PasswordEncoder.class); UserDetailsService userDetailsService = mock(UserDetailsService.class); UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); @@ -296,7 +306,8 @@ public void authenticateWhenBadCredentialsAndPasswordManagerThenNoUpdate() { @Test public void authenticateWhenNotUpgradeAndPasswordManagerThenNoUpdate() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); PasswordEncoder encoder = mock(PasswordEncoder.class); UserDetailsService userDetailsService = mock(UserDetailsService.class); UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); @@ -314,7 +325,7 @@ public void authenticateWhenNotUpgradeAndPasswordManagerThenNoUpdate() { @Test public void testDetectsNullBeingReturnedFromAuthenticationDao() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); DaoAuthenticationProvider provider = createProvider(); provider.setUserDetailsService(new MockUserDetailsServiceReturnsNull()); assertThatExceptionOfType(AuthenticationServiceException.class).isThrownBy(() -> provider.authenticate(token)) @@ -335,7 +346,7 @@ public void testGettersSetters() { @Test public void testGoesBackToAuthenticationDaoToObtainLatestPasswordIfCachedPasswordSeemsIncorrect() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); MockUserDetailsServiceUserRod authenticationDao = new MockUserDetailsServiceUserRod(); MockUserCache cache = new MockUserCache(); DaoAuthenticationProvider provider = createProvider(); @@ -348,7 +359,7 @@ public void testGoesBackToAuthenticationDaoToObtainLatestPasswordIfCachedPasswor // Now change the password the AuthenticationDao will return authenticationDao.setPassword("easternLongNeckTurtle"); // Now try authentication again, with the new password - token = new UsernamePasswordAuthenticationToken("rod", "easternLongNeckTurtle"); + token = UsernamePasswordAuthenticationToken.unauthenticated("rod", "easternLongNeckTurtle"); provider.authenticate(token); // To get this far, the new password was accepted // Check the cache was updated @@ -390,7 +401,8 @@ public void testSupports() { // SEC-2056 @Test public void testUserNotFoundEncodesPassword() throws Exception { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("missing", + "koala"); PasswordEncoder encoder = mock(PasswordEncoder.class); given(encoder.encode(anyString())).willReturn("koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); @@ -406,7 +418,8 @@ public void testUserNotFoundEncodesPassword() throws Exception { @Test public void testUserNotFoundBCryptPasswordEncoder() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", "koala"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("missing", + "koala"); PasswordEncoder encoder = new BCryptPasswordEncoder(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); @@ -419,7 +432,8 @@ public void testUserNotFoundBCryptPasswordEncoder() { @Test public void testUserNotFoundDefaultEncoder() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", null); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("missing", + null); DaoAuthenticationProvider provider = createProvider(); provider.setHideUserNotFoundExceptions(false); provider.setUserDetailsService(new MockUserDetailsServiceUserRod()); @@ -432,8 +446,10 @@ public void testUserNotFoundDefaultEncoder() { * SEC-2056 is fixed. */ public void IGNOREtestSec2056() { - UsernamePasswordAuthenticationToken foundUser = new UsernamePasswordAuthenticationToken("rod", "koala"); - UsernamePasswordAuthenticationToken notFoundUser = new UsernamePasswordAuthenticationToken("notFound", "koala"); + UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod", + "koala"); + UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken + .unauthenticated("notFound", "koala"); PasswordEncoder encoder = new BCryptPasswordEncoder(10, new SecureRandom()); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); @@ -467,7 +483,8 @@ private double avg(List counts) { @Test public void testUserNotFoundNullCredentials() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", null); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("missing", + null); PasswordEncoder encoder = mock(PasswordEncoder.class); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); diff --git a/core/src/test/java/org/springframework/security/authentication/event/AuthenticationEventTests.java b/core/src/test/java/org/springframework/security/authentication/event/AuthenticationEventTests.java index f2ecf729c82..605a1615467 100644 --- a/core/src/test/java/org/springframework/security/authentication/event/AuthenticationEventTests.java +++ b/core/src/test/java/org/springframework/security/authentication/event/AuthenticationEventTests.java @@ -34,8 +34,8 @@ public class AuthenticationEventTests { private Authentication getAuthentication() { - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("Principal", - "Credentials"); + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken + .unauthenticated("Principal", "Credentials"); authentication.setDetails("127.0.0.1"); return authentication; } diff --git a/core/src/test/java/org/springframework/security/authentication/event/LoggerListenerTests.java b/core/src/test/java/org/springframework/security/authentication/event/LoggerListenerTests.java index 1efd1e083e4..07133b8864f 100644 --- a/core/src/test/java/org/springframework/security/authentication/event/LoggerListenerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/event/LoggerListenerTests.java @@ -30,8 +30,8 @@ public class LoggerListenerTests { private Authentication getAuthentication() { - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("Principal", - "Credentials"); + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken + .unauthenticated("Principal", "Credentials"); authentication.setDetails("127.0.0.1"); return authentication; } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java index 4aa88621116..3df2268f58b 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2016 the original author or authors. + * Copyright 2010-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. @@ -79,7 +79,7 @@ public void setUp() throws Exception { new AppConfigurationEntry(TestLoginModule.class.getName(), LoginModuleControlFlag.REQUIRED, Collections.emptyMap()) }; given(configuration.getAppConfigurationEntry(this.provider.getLoginContextName())).willReturn(aces); - this.token = new UsernamePasswordAuthenticationToken("user", "password"); + this.token = UsernamePasswordAuthenticationToken.unauthenticated("user", "password"); ReflectionTestUtils.setField(this.provider, "log", this.log); } @@ -113,15 +113,15 @@ public void authenticateSuccess() { @Test public void authenticateBadPassword() { - assertThatExceptionOfType(AuthenticationException.class) - .isThrownBy(() -> this.provider.authenticate(new UsernamePasswordAuthenticationToken("user", "asdf"))); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy( + () -> this.provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "asdf"))); verifyFailedLogin(); } @Test public void authenticateBadUser() { - assertThatExceptionOfType(AuthenticationException.class).isThrownBy( - () -> this.provider.authenticate(new UsernamePasswordAuthenticationToken("asdf", "password"))); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> this.provider + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("asdf", "password"))); verifyFailedLogin(); } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java index 46ade0722d8..4da9805811e 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java @@ -75,8 +75,8 @@ public void setUp() { @Test public void testBadPassword() { - assertThatExceptionOfType(AuthenticationException.class).isThrownBy( - () -> this.jaasProvider.authenticate(new UsernamePasswordAuthenticationToken("user", "asdf"))); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> this.jaasProvider + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "asdf"))); assertThat(this.eventCheck.failedEvent).as("Failure event not fired").isNotNull(); assertThat(this.eventCheck.failedEvent.getException()).withFailMessage("Failure event exception was null") .isNotNull(); @@ -85,8 +85,8 @@ public void testBadPassword() { @Test public void testBadUser() { - assertThatExceptionOfType(AuthenticationException.class).isThrownBy( - () -> this.jaasProvider.authenticate(new UsernamePasswordAuthenticationToken("asdf", "password"))); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> this.jaasProvider + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("asdf", "password"))); assertThat(this.eventCheck.failedEvent).as("Failure event not fired").isNotNull(); assertThat(this.eventCheck.failedEvent.getException()).withFailMessage("Failure event exception was null") .isNotNull(); @@ -158,8 +158,8 @@ public void detectsMissingLoginContextName() throws Exception { @Test public void testFull() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password", - AuthorityUtils.createAuthorityList("ROLE_ONE")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("user", + "password", AuthorityUtils.createAuthorityList("ROLE_ONE")); assertThat(this.jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue(); Authentication auth = this.jaasProvider.authenticate(token); assertThat(this.jaasProvider.getAuthorityGranters()).isNotNull(); @@ -198,7 +198,7 @@ public void testLoginExceptionResolver() { assertThat(this.jaasProvider.getLoginExceptionResolver()).isNotNull(); this.jaasProvider.setLoginExceptionResolver((e) -> new LockedException("This is just a test!")); try { - this.jaasProvider.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + this.jaasProvider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); } catch (LockedException ex) { } @@ -221,7 +221,8 @@ public void testLogout() throws Exception { @Test public void testNullDefaultAuthorities() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); assertThat(this.jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue(); Authentication auth = this.jaasProvider.authenticate(token); assertThat(auth.getAuthorities()).withFailMessage("Only ROLE_TEST1 and ROLE_TEST2 should have been returned") diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/Sec760Tests.java b/core/src/test/java/org/springframework/security/authentication/jaas/Sec760Tests.java index 6dd80ffe06c..8fe9cdfa227 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/Sec760Tests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/Sec760Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -56,8 +56,8 @@ private void testConfigureJaasCase(JaasAuthenticationProvider p1, JaasAuthentica } private void testAuthenticate(JaasAuthenticationProvider p1) { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password", - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated("user", + "password", AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); Authentication auth = p1.authenticate(token); assertThat(auth).isNotNull(); } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java index 9b631a303a5..293d85bd448 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java @@ -44,7 +44,7 @@ public class SecurityContextLoginModuleTests { private Subject subject = new Subject(false, new HashSet<>(), new HashSet<>(), new HashSet<>()); - private UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("principal", + private UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.unauthenticated("principal", "credentials"); @BeforeEach diff --git a/core/src/test/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProviderTests.java index e2276352949..2cac2be22a9 100644 --- a/core/src/test/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/rcp/RemoteAuthenticationProviderTests.java @@ -40,8 +40,8 @@ public class RemoteAuthenticationProviderTests { public void testExceptionsGetPassedBackToCaller() { RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider(); provider.setRemoteAuthenticationManager(new MockRemoteAuthenticationManager(false)); - assertThatExceptionOfType(RemoteAuthenticationException.class) - .isThrownBy(() -> provider.authenticate(new UsernamePasswordAuthenticationToken("rod", "password"))); + assertThatExceptionOfType(RemoteAuthenticationException.class).isThrownBy( + () -> provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", "password"))); } @Test @@ -63,7 +63,8 @@ public void testStartupChecksAuthenticationManagerSet() throws Exception { public void testSuccessfulAuthenticationCreatesObject() { RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider(); provider.setRemoteAuthenticationManager(new MockRemoteAuthenticationManager(true)); - Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("rod", "password")); + Authentication result = provider + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", "password")); assertThat(result.getPrincipal()).isEqualTo("rod"); assertThat(result.getCredentials()).isEqualTo("password"); assertThat(AuthorityUtils.authorityListToSet(result.getAuthorities())).contains("foo"); @@ -73,8 +74,8 @@ public void testSuccessfulAuthenticationCreatesObject() { public void testNullCredentialsDoesNotCauseNullPointerException() { RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider(); provider.setRemoteAuthenticationManager(new MockRemoteAuthenticationManager(false)); - assertThatExceptionOfType(RemoteAuthenticationException.class) - .isThrownBy(() -> provider.authenticate(new UsernamePasswordAuthenticationToken("rod", null))); + assertThatExceptionOfType(RemoteAuthenticationException.class).isThrownBy( + () -> provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", null))); } @Test diff --git a/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java index fa6b817768f..fc89bc3760c 100644 --- a/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java +++ b/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java @@ -76,8 +76,8 @@ public void testNotEqualsDueToAbstractParentEqualsCheck() { @Test public void testNotEqualsDueToDifferentAuthenticationClass() { RememberMeAuthenticationToken token1 = new RememberMeAuthenticationToken("key", "Test", ROLES_12); - UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test", "Password", - ROLES_12); + UsernamePasswordAuthenticationToken token2 = UsernamePasswordAuthenticationToken.authenticated("Test", + "Password", ROLES_12); assertThat(token1.equals(token2)).isFalse(); } diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java index 43d8d0631cf..7054910334d 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,17 @@ package org.springframework.security.authorization; +import java.util.Collections; import java.util.function.Supplier; import org.junit.jupiter.api.Test; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; 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 +138,30 @@ public void hasAuthorityWhenUserHasNotAuthorityThenDeniedDecision() { assertThat(manager.check(authentication, object).isGranted()).isFalse(); } + @Test + public void hasAuthorityWhenUserHasCustomAuthorityThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + GrantedAuthority customGrantedAuthority = () -> "ADMIN"; + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + Collections.singletonList(customGrantedAuthority)); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasAuthorityWhenUserHasNotCustomAuthorityThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + GrantedAuthority customGrantedAuthority = () -> "USER"; + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + Collections.singletonList(customGrantedAuthority)); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + @Test public void hasAnyRoleWhenUserHasAnyRoleThenGrantedDecision() { AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER"); @@ -185,4 +214,37 @@ public void hasAnyAuthorityWhenUserHasNotAnyAuthorityThenDeniedDecision() { assertThat(manager.check(authentication, object).isGranted()).isFalse(); } + @Test + public void setRoleHierarchyWhenNullThenIllegalArgumentException() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("USER"); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setRoleHierarchy(null)) + .withMessage("roleHierarchy cannot be null"); + } + + @Test + public void setRoleHierarchyWhenNotNullThenVerifyRoleHierarchy() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("USER"); + RoleHierarchy roleHierarchy = new RoleHierarchyImpl(); + manager.setRoleHierarchy(roleHierarchy); + assertThat(manager).extracting("roleHierarchy").isEqualTo(roleHierarchy); + } + + @Test + public void getRoleHierarchyWhenNotSetThenDefaultsToNullRoleHierarchy() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("USER"); + assertThat(manager).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); + } + + @Test + public void hasRoleWhenRoleHierarchySetThenGreaterRoleTakesPrecedence() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("USER"); + RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); + roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); + manager.setRoleHierarchy(roleHierarchy); + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "ROLE_ADMIN"); + Object object = new Object(); + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java index 2fd6ac42e43..ac937cfbf62 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java @@ -27,6 +27,7 @@ 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 void checkWhenHasAuthorityAndAuthorizedThenReturnTrue() { 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"); diff --git a/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java b/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java new file mode 100644 index 00000000000..cc56d8477e1 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.event.AuthorizationDeniedEvent; +import org.springframework.security.core.Authentication; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link SpringAuthorizationEventPublisher} + * + * @author Parikshit Dutta + */ +public class SpringAuthorizationEventPublisherTests { + + Supplier authentication = () -> TestAuthentication.authenticatedUser(); + + ApplicationEventPublisher applicationEventPublisher; + + SpringAuthorizationEventPublisher authorizationEventPublisher; + + @BeforeEach + public void init() { + this.applicationEventPublisher = mock(ApplicationEventPublisher.class); + this.authorizationEventPublisher = new SpringAuthorizationEventPublisher(this.applicationEventPublisher); + } + + @Test + public void testAuthenticationSuccessIsNotPublished() { + AuthorizationDecision decision = new AuthorizationDecision(true); + this.authorizationEventPublisher.publishAuthorizationEvent(this.authentication, mock(Object.class), decision); + verifyNoInteractions(this.applicationEventPublisher); + } + + @Test + public void testAuthenticationFailureIsPublished() { + AuthorizationDecision decision = new AuthorizationDecision(false); + this.authorizationEventPublisher.publishAuthorizationEvent(this.authentication, mock(Object.class), decision); + verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationDeniedEvent.class)); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java index 2a129cb56a2..63f531d3c5e 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,20 @@ package org.springframework.security.authorization.method; +import java.util.function.Supplier; + import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; import org.springframework.aop.Pointcut; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -66,4 +75,32 @@ public void beforeWhenMockAuthorizationManagerThenCheckAndReturnedObject() throw any(MethodInvocationResult.class)); } + @Test + public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() { + AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor( + Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated()); + assertThatIllegalArgumentException().isThrownBy(() -> advice.setAuthorizationEventPublisher(null)) + .withMessage("eventPublisher cannot be null"); + } + + @Test + public void invokeWhenAuthorizationEventPublisherThenUses() throws Throwable { + AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor( + Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated()); + AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class); + advice.setAuthorizationEventPublisher(eventPublisher); + + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + SecurityContextHolder.setContext(securityContext); + + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + MethodInvocationResult result = new MethodInvocationResult(mockMethodInvocation, new Object()); + given(mockMethodInvocation.proceed()).willReturn(result.getResult()); + + advice.invoke(mockMethodInvocation); + verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(MethodInvocationResult.class), + any(AuthorizationDecision.class)); + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java index 9065e110407..f4a6a62fc4e 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,24 @@ package org.springframework.security.authorization.method; +import java.util.function.Supplier; + import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; import org.springframework.aop.Pointcut; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -59,4 +70,32 @@ public void beforeWhenMockAuthorizationManagerThenCheck() throws Throwable { mockMethodInvocation); } + @Test + public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() { + AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor( + Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated()); + assertThatIllegalArgumentException().isThrownBy(() -> advice.setAuthorizationEventPublisher(null)) + .withMessage("eventPublisher cannot be null"); + } + + @Test + public void invokeWhenAuthorizationEventPublisherThenUses() throws Throwable { + AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor( + Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated()); + AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class); + advice.setAuthorizationEventPublisher(eventPublisher); + + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + SecurityContextHolder.setContext(securityContext); + + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + MethodInvocationResult result = new MethodInvocationResult(mockMethodInvocation, new Object()); + given(mockMethodInvocation.proceed()).willReturn(result.getResult()); + + advice.invoke(mockMethodInvocation); + verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(MethodInvocation.class), + any(AuthorizationDecision.class)); + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java index d570923666d..83cbe5cdb93 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.aop.TargetClassAware; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; @@ -133,6 +134,19 @@ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationE .isThrownBy(() -> manager.check(authentication, methodInvocation)); } + @Test + public void checkTargetClassAwareWhenInterfaceLevelAnnotationsThenApplies() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestTargetClassAware(), + TestTargetClassAware.class, "doSomething"); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public void doSomething() { @@ -198,4 +212,33 @@ public interface InterfaceAnnotationsThree { } + @PreAuthorize("hasRole('ADMIN')") + public interface InterfaceLevelAnnotations { + + } + + public static class TestTargetClassAware extends TestClass implements TargetClassAware, InterfaceLevelAnnotations { + + @Override + public Class getTargetClass() { + return TestClass.class; + } + + @Override + public void doSomething() { + super.doSomething(); + } + + @Override + public String doSomethingString(String s) { + return super.doSomethingString(s); + } + + @Override + public void inheritedAnnotations() { + super.inheritedAnnotations(); + } + + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java index db730feb361..f546d8cb033 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.aop.TargetClassAware; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.intercept.method.MockMethodInvocation; @@ -127,6 +128,19 @@ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationE .isThrownBy(() -> manager.check(authentication, methodInvocation)); } + @Test + public void checkTargetClassAwareWhenInterfaceLevelAnnotationsThenApplies() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestTargetClassAware(), + TestTargetClassAware.class, "doSomething"); + SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public void doSomething() { @@ -192,4 +206,33 @@ public interface InterfaceAnnotationsThree { } + @Secured("ROLE_ADMIN") + public interface InterfaceLevelAnnotations { + + } + + public static class TestTargetClassAware extends TestClass implements TargetClassAware, InterfaceLevelAnnotations { + + @Override + public Class getTargetClass() { + return TestClass.class; + } + + @Override + public void doSomething() { + super.doSomething(); + } + + @Override + public void securedUserOrAdmin() { + super.securedUserOrAdmin(); + } + + @Override + public void inheritedAnnotations() { + super.inheritedAnnotations(); + } + + } + } diff --git a/core/src/test/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategyTests.java b/core/src/test/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategyTests.java index 999fee6fd66..f84cfbc43aa 100644 --- a/core/src/test/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategyTests.java +++ b/core/src/test/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategyTests.java @@ -55,8 +55,8 @@ public void setContextWhenNoChangeToContextThenListenersAreNotNotified() { @Test public void constructorWhenNullDelegateThenIllegalArgument() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> new ListeningSecurityContextHolderStrategy(null, (event) -> { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> new ListeningSecurityContextHolderStrategy((SecurityContextHolderStrategy) null, (event) -> { })); } diff --git a/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java b/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java index 563f7a307ad..6aecd517ac2 100644 --- a/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java +++ b/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java @@ -41,7 +41,7 @@ public final void setUp() { @Test public void testContextHolderGetterSetterClearer() { SecurityContext sc = new SecurityContextImpl(); - sc.setAuthentication(new UsernamePasswordAuthenticationToken("Foobar", "pass")); + sc.setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("Foobar", "pass")); SecurityContextHolder.setContext(sc); assertThat(SecurityContextHolder.getContext()).isEqualTo(sc); SecurityContextHolder.clearContext(); diff --git a/core/src/test/java/org/springframework/security/core/context/SecurityContextImplTests.java b/core/src/test/java/org/springframework/security/core/context/SecurityContextImplTests.java index 645151b7cff..3e15ea57234 100644 --- a/core/src/test/java/org/springframework/security/core/context/SecurityContextImplTests.java +++ b/core/src/test/java/org/springframework/security/core/context/SecurityContextImplTests.java @@ -40,7 +40,7 @@ public void testEmptyObjectsAreEquals() { @Test public void testSecurityContextCorrectOperation() { SecurityContext context = new SecurityContextImpl(); - Authentication auth = new UsernamePasswordAuthenticationToken("rod", "koala"); + Authentication auth = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); context.setAuthentication(auth); assertThat(context.getAuthentication()).isEqualTo(auth); assertThat(context.toString().lastIndexOf("rod") != -1).isTrue(); diff --git a/core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java index ced0820357c..524d75d99d7 100644 --- a/core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java +++ b/core/src/test/java/org/springframework/security/jackson2/SecurityContextMixinTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-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. @@ -47,7 +47,7 @@ public class SecurityContextMixinTests extends AbstractMixinTests { @Test public void securityContextSerializeTest() throws JsonProcessingException, JSONException { SecurityContext context = new SecurityContextImpl(); - context.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "1234", + context.setAuthentication(UsernamePasswordAuthenticationToken.authenticated("admin", "1234", Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")))); String actualJson = this.mapper.writeValueAsString(context); JSONAssert.assertEquals(SECURITY_CONTEXT_JSON, actualJson, true); diff --git a/core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java b/core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java new file mode 100644 index 00000000000..9666b8a9e94 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson2; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +class UnmodifiableMapDeserializerTests extends AbstractMixinTests { + + // @formatter:off + private static final String DEFAULT_MAP_JSON = "{" + + "\"@class\": \"java.util.Collections$UnmodifiableMap\"," + + "\"Key\": \"Value\"" + + "}"; + // @formatter:on + + @Test + void shouldSerialize() throws Exception { + String mapJson = mapper + .writeValueAsString(Collections.unmodifiableMap(Collections.singletonMap("Key", "Value"))); + + JSONAssert.assertEquals(DEFAULT_MAP_JSON, mapJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Map map = mapper.readValue(DEFAULT_MAP_JSON, + Collections.unmodifiableMap(Collections.emptyMap()).getClass()); + + assertThat(map).isNotNull().isInstanceOf(Collections.unmodifiableMap(Collections.emptyMap()).getClass()) + .containsAllEntriesOf(Collections.singletonMap("Key", "Value")); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java index f28bfae6e9e..21d8815642e 100644 --- a/core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java +++ b/core/src/test/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenMixinTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-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. @@ -71,7 +71,8 @@ public class UsernamePasswordAuthenticationTokenMixinTests extends AbstractMixin @Test public void serializeUnauthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws JsonProcessingException, JSONException { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("admin", "1234"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("admin", + "1234"); String serializedJson = this.mapper.writeValueAsString(token); JSONAssert.assertEquals(UNAUTHENTICATED_STRINGPRINCIPAL_JSON, serializedJson, true); } @@ -80,8 +81,8 @@ public void serializeUnauthenticatedUsernamePasswordAuthenticationTokenMixinTest public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() throws JsonProcessingException, JSONException { User user = createDefaultUser(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), - user.getPassword(), user.getAuthorities()); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .authenticated(user.getUsername(), user.getPassword(), user.getAuthorities()); String serializedJson = this.mapper.writeValueAsString(token); JSONAssert.assertEquals(AUTHENTICATED_STRINGPRINCIPAL_JSON, serializedJson, true); } @@ -140,7 +141,7 @@ public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinWithNo throws JsonProcessingException, JSONException { NonUserPrincipal principal = new NonUserPrincipal(); principal.setUsername("admin"); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, null, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(principal, null, new ArrayList<>()); String actualJson = this.mapper.writeValueAsString(token); JSONAssert.assertEquals(AUTHENTICATED_NON_USER_PRINCIPAL_JSON, actualJson, true); @@ -170,7 +171,8 @@ public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenWithDetai @Test public void serializingThenDeserializingWithNoCredentialsOrDetailsShouldWork() throws IOException { - UsernamePasswordAuthenticationToken original = new UsernamePasswordAuthenticationToken("Frodo", null); + UsernamePasswordAuthenticationToken original = UsernamePasswordAuthenticationToken.unauthenticated("Frodo", + null); String serialized = this.mapper.writeValueAsString(original); UsernamePasswordAuthenticationToken deserialized = this.mapper.readValue(serialized, UsernamePasswordAuthenticationToken.class); @@ -181,7 +183,8 @@ public void serializingThenDeserializingWithNoCredentialsOrDetailsShouldWork() t public void serializingThenDeserializingWithConfiguredObjectMapperShouldWork() throws IOException { this.mapper.setDefaultPropertyInclusion(Value.construct(Include.ALWAYS, Include.NON_NULL)) .setSerializationInclusion(Include.NON_ABSENT); - UsernamePasswordAuthenticationToken original = new UsernamePasswordAuthenticationToken("Frodo", null); + UsernamePasswordAuthenticationToken original = UsernamePasswordAuthenticationToken.unauthenticated("Frodo", + null); String serialized = this.mapper.writeValueAsString(original); UsernamePasswordAuthenticationToken deserialized = this.mapper.readValue(serialized, UsernamePasswordAuthenticationToken.class); @@ -190,8 +193,8 @@ public void serializingThenDeserializingWithConfiguredObjectMapperShouldWork() t private UsernamePasswordAuthenticationToken createToken() { User user = createDefaultUser(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, user.getPassword(), - user.getAuthorities()); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(user, + user.getPassword(), user.getAuthorities()); return token; } diff --git a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java index 4192377632b..a04b84bdc36 100644 --- a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java +++ b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -344,14 +344,14 @@ public void updateUserDoesNotSaveAuthoritiesIfEnableAuthoritiesIsFalse() { @Test public void createNewAuthenticationUsesNullPasswordToKeepPassordsSave() { insertJoe(); - UsernamePasswordAuthenticationToken currentAuth = new UsernamePasswordAuthenticationToken("joe", null, + UsernamePasswordAuthenticationToken currentAuth = UsernamePasswordAuthenticationToken.authenticated("joe", null, AuthorityUtils.createAuthorityList("ROLE_USER")); Authentication updatedAuth = this.manager.createNewAuthentication(currentAuth, "new"); assertThat(updatedAuth.getCredentials()).isNull(); } private Authentication authenticateJoe() { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("joe", "password", + UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.authenticated("joe", "password", joe.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); return auth; diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index d83dec021d7..b9039b1d749 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -53,7 +53,9 @@ * Such that "id" is an identifier used to look up which {@link PasswordEncoder} should be * used and "encodedPassword" is the original encoded password for the selected * {@link PasswordEncoder}. The "id" must be at the beginning of the password, start with - * "{" and end with "}". If the "id" cannot be found, the "id" will be null. + * "{" (id prefix) and end with "}" (id suffix). Both id prefix and id suffix can be + * customized via {@link #DelegatingPasswordEncoder(String, Map, String, String)}. If the + * "id" cannot be found, the "id" will be null. * * For example, the following might be a list of passwords encoded using different "id". * All of the original passwords are "password". @@ -116,14 +118,20 @@ * * @author Rob Winch * @author Michael Simons + * @author heowc + * @author Jihoon Cha * @since 5.0 * @see org.springframework.security.crypto.factory.PasswordEncoderFactories */ public class DelegatingPasswordEncoder implements PasswordEncoder { - private static final String PREFIX = "{"; + private static final String DEFAULT_ID_PREFIX = "{"; - private static final String SUFFIX = "}"; + private static final String DEFAULT_ID_SUFFIX = "}"; + + private final String idPrefix; + + private final String idSuffix; private final String idForEncode; @@ -142,9 +150,34 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { * {@link #matches(CharSequence, String)} */ public DelegatingPasswordEncoder(String idForEncode, Map idToPasswordEncoder) { + this(idForEncode, idToPasswordEncoder, DEFAULT_ID_PREFIX, DEFAULT_ID_SUFFIX); + } + + /** + * Creates a new instance + * @param idForEncode the id used to lookup which {@link PasswordEncoder} should be + * used for {@link #encode(CharSequence)} + * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine + * which {@link PasswordEncoder} should be used for + * @param idPrefix the prefix that denotes the start of the id in the encoded results + * @param idSuffix the suffix that denotes the end of an id in the encoded results + * {@link #matches(CharSequence, String)} + */ + public DelegatingPasswordEncoder(String idForEncode, Map idToPasswordEncoder, + String idPrefix, String idSuffix) { if (idForEncode == null) { throw new IllegalArgumentException("idForEncode cannot be null"); } + if (idPrefix == null) { + throw new IllegalArgumentException("prefix cannot be null"); + } + if (idSuffix == null || idSuffix.isEmpty()) { + throw new IllegalArgumentException("suffix cannot be empty"); + } + if (idPrefix.contains(idSuffix)) { + throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix); + } + if (!idToPasswordEncoder.containsKey(idForEncode)) { throw new IllegalArgumentException( "idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder); @@ -153,16 +186,18 @@ public DelegatingPasswordEncoder(String idForEncode, Map(idToPasswordEncoder); + this.idPrefix = idPrefix; + this.idSuffix = idSuffix; } /** @@ -188,7 +223,7 @@ public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordE @Override public String encode(CharSequence rawPassword) { - return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword); + return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword); } @Override @@ -209,15 +244,15 @@ private String extractId(String prefixEncodedPassword) { if (prefixEncodedPassword == null) { return null; } - int start = prefixEncodedPassword.indexOf(PREFIX); + int start = prefixEncodedPassword.indexOf(this.idPrefix); if (start != 0) { return null; } - int end = prefixEncodedPassword.indexOf(SUFFIX, start); + int end = prefixEncodedPassword.indexOf(this.idSuffix, start); if (end < 0) { return null; } - return prefixEncodedPassword.substring(start + 1, end); + return prefixEncodedPassword.substring(start + this.idPrefix.length(), end); } @Override @@ -233,8 +268,8 @@ public boolean upgradeEncoding(String prefixEncodedPassword) { } private String extractEncodedPassword(String prefixEncodedPassword) { - int start = prefixEncodedPassword.indexOf(SUFFIX); - return prefixEncodedPassword.substring(start + 1); + int start = prefixEncodedPassword.indexOf(this.idSuffix); + return prefixEncodedPassword.substring(start + this.idSuffix.length()); } /** diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java index 9f3c3f18ac7..48dd89e28c1 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -36,6 +36,8 @@ /** * @author Rob Winch * @author Michael Simons + * @author heowc + * @author Jihoon Cha * @since 5.0 */ @ExtendWith(MockitoExtension.class) @@ -64,12 +66,16 @@ public class DelegatingPasswordEncoderTests { private DelegatingPasswordEncoder passwordEncoder; + private DelegatingPasswordEncoder onlySuffixPasswordEncoder; + @BeforeEach public void setup() { this.delegates = new HashMap<>(); this.delegates.put(this.bcryptId, this.bcrypt); this.delegates.put("noop", this.noop); this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates); + + this.onlySuffixPasswordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$"); } @Test @@ -83,6 +89,55 @@ public void constructorWhenIdForEncodeDoesNotExistThenIllegalArgumentException() .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId + "INVALID", this.delegates)); } + @Test + public void constructorWhenPrefixIsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, null, "$")); + } + + @Test + public void constructorWhenSuffixIsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", null)); + } + + @Test + public void constructorWhenPrefixIsEmpty() { + assertThat(new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$")).isNotNull(); + } + + @Test + public void constructorWhenSuffixIsEmpty() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", "")); + } + + @Test + public void constructorWhenPrefixAndSuffixAreEmpty() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "")); + } + + @Test + public void constructorWhenIdContainsPrefixThenIllegalArgumentException() { + this.delegates.put('{' + this.bcryptId, this.bcrypt); + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates)); + } + + @Test + public void constructorWhenIdContainsSuffixThenIllegalArgumentException() { + this.delegates.put(this.bcryptId + '$', this.bcrypt); + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$")); + } + + @Test + public void constructorWhenPrefixContainsSuffixThenIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", "$")); + } + @Test public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() { assertThatIllegalArgumentException() @@ -104,6 +159,12 @@ public void encodeWhenValidThenUsesIdForEncode() { assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword); } + @Test + public void encodeWhenValidBySpecifyDelegatingPasswordEncoderThenUsesIdForEncode() { + given(this.bcrypt.encode(this.rawPassword)).willReturn(this.encodedPassword); + assertThat(this.onlySuffixPasswordEncoder.encode(this.rawPassword)).isEqualTo("bcrypt$" + this.encodedPassword); + } + @Test public void matchesWhenBCryptThenDelegatesToBCrypt() { given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true); @@ -112,6 +173,14 @@ public void matchesWhenBCryptThenDelegatesToBCrypt() { verifyZeroInteractions(this.noop); } + @Test + public void matchesWhenBCryptBySpecifyDelegatingPasswordEncoderThenDelegatesToBCrypt() { + given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true); + assertThat(this.onlySuffixPasswordEncoder.matches(this.rawPassword, "bcrypt$" + this.encodedPassword)).isTrue(); + verify(this.bcrypt).matches(this.rawPassword, this.encodedPassword); + verifyZeroInteractions(this.noop); + } + @Test public void matchesWhenNoopThenDelegatesToNoop() { given(this.noop.matches(this.rawPassword, this.encodedPassword)).willReturn(true); diff --git a/data/spring-security-data.gradle b/data/spring-security-data.gradle index e0c9f14dab7..3e915ef871d 100644 --- a/data/spring-security-data.gradle +++ b/data/spring-security-data.gradle @@ -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' diff --git a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java index 3696904a9a9..a4ec7b00d22 100644 --- a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java +++ b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -17,10 +17,17 @@ package org.springframework.security.data.repository.query; import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.DenyAllPermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; /** *

      @@ -77,12 +84,21 @@ * it. * * @author Rob Winch + * @author Evgeniy Cheban * @since 4.0 */ public class SecurityEvaluationContextExtension implements EvaluationContextExtension { private Authentication authentication; + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + + private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); + + private String defaultRolePrefix = "ROLE_"; + /** * Creates a new instance that uses the current {@link Authentication} found on the * {@link org.springframework.security.core.context.SecurityContextHolder}. @@ -106,8 +122,13 @@ public String getExtensionId() { @Override public SecurityExpressionRoot getRootObject() { Authentication authentication = getAuthentication(); - return new SecurityExpressionRoot(authentication) { + SecurityExpressionRoot root = new SecurityExpressionRoot(authentication) { }; + root.setTrustResolver(this.trustResolver); + root.setRoleHierarchy(this.roleHierarchy); + root.setPermissionEvaluator(this.permissionEvaluator); + root.setDefaultRolePrefix(this.defaultRolePrefix); + return root; } private Authentication getAuthentication() { @@ -118,4 +139,52 @@ private Authentication getAuthentication() { return context.getAuthentication(); } + /** + * Sets the {@link AuthenticationTrustResolver} to be used. Default is + * {@link AuthenticationTrustResolverImpl}. Cannot be null. + * @param trustResolver the {@link AuthenticationTrustResolver} to use + * @since 5.8 + */ + public void setTrustResolver(AuthenticationTrustResolver trustResolver) { + Assert.notNull(trustResolver, "trustResolver cannot be null"); + this.trustResolver = trustResolver; + } + + /** + * Sets the {@link RoleHierarchy} to be used. Default is {@link NullRoleHierarchy}. + * Cannot be null. + * @param roleHierarchy the {@link RoleHierarchy} to use + * @since 5.8 + */ + public void setRoleHierarchy(RoleHierarchy roleHierarchy) { + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; + } + + /** + * Sets the {@link PermissionEvaluator} to be used. Default is + * {@link DenyAllPermissionEvaluator}. Cannot be null. + * @param permissionEvaluator the {@link PermissionEvaluator} to use + * @since 5.8 + */ + public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { + Assert.notNull(permissionEvaluator, "permissionEvaluator cannot be null"); + this.permissionEvaluator = permissionEvaluator; + } + + /** + * Sets the default prefix to be added to + * {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyRole(String...)} + * or + * {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasRole(String)}. + * For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN") is passed in, then the + * role ROLE_ADMIN will be used when the defaultRolePrefix is "ROLE_" (default). + * @param defaultRolePrefix the default prefix to add to roles. The default is + * "ROLE_". + * @since 5.8 + */ + public void setDefaultRolePrefix(String defaultRolePrefix) { + this.defaultRolePrefix = defaultRolePrefix; + } + } diff --git a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java index a890a463af4..293c114fb11 100644 --- a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java +++ b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -20,7 +20,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.DenyAllPermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -69,6 +75,77 @@ public void getRootObjectExplicitAuthentication() { assertThat(getRoot().getAuthentication()).isSameAs(explicit); } + @Test + public void setTrustResolverWhenNullThenIllegalArgumentException() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + assertThatIllegalArgumentException().isThrownBy(() -> this.securityExtension.setTrustResolver(null)) + .withMessage("trustResolver cannot be null"); + } + + @Test + public void setTrustResolverWhenNotNullThenVerifyRootObject() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + this.securityExtension.setTrustResolver(trustResolver); + assertThat(getRoot()).extracting("trustResolver").isEqualTo(trustResolver); + } + + @Test + public void setRoleHierarchyWhenNullThenIllegalArgumentException() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + assertThatIllegalArgumentException().isThrownBy(() -> this.securityExtension.setRoleHierarchy(null)) + .withMessage("roleHierarchy cannot be null"); + } + + @Test + public void setRoleHierarchyWhenNotNullThenVerifyRootObject() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + this.securityExtension.setRoleHierarchy(roleHierarchy); + assertThat(getRoot()).extracting("roleHierarchy").isEqualTo(roleHierarchy); + } + + @Test + public void setPermissionEvaluatorWhenNullThenIllegalArgumentException() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + assertThatIllegalArgumentException().isThrownBy(() -> this.securityExtension.setPermissionEvaluator(null)) + .withMessage("permissionEvaluator cannot be null"); + } + + @Test + public void setPermissionEvaluatorWhenNotNullThenVerifyRootObject() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); + this.securityExtension.setPermissionEvaluator(permissionEvaluator); + assertThat(getRoot()).extracting("permissionEvaluator").isEqualTo(permissionEvaluator); + } + + @Test + public void setDefaultRolePrefixWhenCustomThenVerifyRootObject() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + String defaultRolePrefix = "CUSTOM_"; + this.securityExtension.setDefaultRolePrefix(defaultRolePrefix); + assertThat(getRoot()).extracting("defaultRolePrefix").isEqualTo(defaultRolePrefix); + } + + @Test + public void getRootObjectWhenAdditionalFieldsNotSetThenVerifyDefaults() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + SecurityExpressionRoot root = getRoot(); + assertThat(root).extracting("trustResolver").isInstanceOf(AuthenticationTrustResolverImpl.class); + assertThat(root).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); + assertThat(root).extracting("permissionEvaluator").isInstanceOf(DenyAllPermissionEvaluator.class); + assertThat(root).extracting("defaultRolePrefix").isEqualTo("ROLE_"); + } + private SecurityExpressionRoot getRoot() { return this.securityExtension.getRootObject(); } diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index b2cc1be458d..bc5decf23d4 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,34 +8,34 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.12") - 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-M1") + 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.2.0-M4") 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("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1") + api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328") constraints { - api "ch.qos.logback:logback-classic:1.2.6" + 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.18" + api "com.nimbusds:nimbus-jose-jwt:9.21" + api "com.nimbusds:oauth2-oidc-sdk:9.34" api "com.squareup.okhttp3:mockwebserver:3.14.9" api "com.squareup.okhttp3:okhttp:3.14.9" 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" + api "net.sourceforge.htmlunit:htmlunit:2.60.0" api "net.sourceforge.nekohtml:nekohtml:1.9.22" api "org.apache.directory.server:apacheds-core-entry:1.5.5" api "org.apache.directory.server:apacheds-core:1.5.5" @@ -46,16 +46,16 @@ dependencies { api "org.apache.httpcomponents:httpclient:4.5.13" api "org.aspectj:aspectjrt:$aspectjVersion" api "org.aspectj:aspectjweaver:$aspectjVersion" - 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.assertj:assertj-core:3.22.0" + api "org.bouncycastle:bcpkix-jdk15on:1.70" + api "org.bouncycastle:bcprov-jdk15on:1.70" + api "org.eclipse.jetty:jetty-server:9.4.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.0.Final" - api "org.hsqldb:hsqldb:2.6.0" - api "org.jasig.cas.client:cas-client-core:3.6.2" + 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.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" @@ -64,14 +64,13 @@ dependencies { api "org.opensaml:opensaml-saml-api:$openSamlVersion" api "org.opensaml:opensaml-saml-impl:$openSamlVersion" api "org.python:jython:2.5.3" - api "org.seleniumhq.selenium:htmlunit-driver:2.54.0" + api "org.seleniumhq.selenium:htmlunit-driver:2.60.0" 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.4.0-M1" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml new file mode 100644 index 00000000000..93f0e0b74f1 --- /dev/null +++ b/docs/antora-playbook.yml @@ -0,0 +1,27 @@ +site: + title: Spring Security + url: https://docs.spring.io/spring-security/reference/ +asciidoc: + attributes: + page-pagination: true +content: + sources: + - url: https://github.com/spring-io/spring-generated-docs + branches: [spring-projects/spring-security/*] + - url: https://github.com/spring-projects/spring-security + branches: [main,5.6.x,5.7.x,5.8.x] + tags: ['5.6.*','!5.6.0-M*','5.7.*','5.8.*','6.0.*'] + start_path: docs +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip + snapshot: true + +antora: + extensions: + - require: ./antora/extensions/version-fix.js + - require: ./antora/extensions/major-minor-segment.js diff --git a/docs/antora.yml b/docs/antora.yml index 0237a48024c..f4f479b8d1f 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,5 +1,3 @@ name: ROOT -title: Spring Security -start_page: ROOT:index.adoc -nav: -- modules/ROOT/nav.adoc +version: '5.8.0' +prerelease: '-SNAPSHOT' diff --git a/docs/antora/extensions/major-minor-segment.js b/docs/antora/extensions/major-minor-segment.js new file mode 100644 index 00000000000..3c3761f9236 --- /dev/null +++ b/docs/antora/extensions/major-minor-segment.js @@ -0,0 +1,200 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + +const { posix: path } = require('path') + +module.exports.register = function({ config }) { + this.on('contentClassified', ({ contentCatalog }) => { + contentCatalog.getComponents().forEach(component => { + const componentName = component.name; + const generationToVersion = new Map(); + component.versions.forEach(version => { + const generation = getGeneration(version.version); + const original = generationToVersion.get(generation); + if (original === undefined || (original.prerelease && !version.prerelease)) { + generationToVersion.set(generation, version); + } + }); + + const versionToGeneration = Array.from(generationToVersion.entries()).reduce((acc, entry) => { + const [ generation, version ] = entry; + acc.set(version.version, generation); + return acc; + }, new Map()); + + contentCatalog.findBy({ component: componentName }).forEach((file) => { + const candidateVersion = file.src.version; + if (versionToGeneration.has(candidateVersion)) { + const generation = versionToGeneration.get(candidateVersion); + if (file.out) { + if (file.out) { + file.out.dirname = file.out.dirname.replace(candidateVersion, generation) + file.out.path = file.out.path.replace(candidateVersion, generation); + } + } + if (file.pub) { + file.pub.url = file.pub.url.replace(candidateVersion, generation) + } + } + }); + versionToGeneration.forEach((generation, mappedVersion) => { + contentCatalog.getComponent(componentName).versions.filter(version => version.version === mappedVersion).forEach((version) => { + version.url = version.url.replace(mappedVersion, generation); + }) + const symbolicVersionAlias = createSymbolicVersionAlias( + componentName, + mappedVersion, + generation, + 'redirect:to' + ) + symbolicVersionAlias.src.version = generation; + contentCatalog.addFile(symbolicVersionAlias); + }); + }) + }) +} + +function createSymbolicVersionAlias (component, version, symbolicVersionSegment, strategy) { + if (symbolicVersionSegment == null || symbolicVersionSegment === version) return + const family = 'alias' + const baseVersionAliasSrc = { component, module: 'ROOT', family, relative: '', basename: '', stem: '', extname: '' } + const symbolicVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version: symbolicVersionSegment }) + const symbolicVersionAlias = { + src: symbolicVersionAliasSrc, + pub: computePub( + symbolicVersionAliasSrc, + computeOut(symbolicVersionAliasSrc, family, symbolicVersionSegment), + family + ), + } + const originalVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version }) + const originalVersionSegment = computeVersionSegment(component, version, 'original') + const originalVersionAlias = { + src: originalVersionAliasSrc, + pub: computePub( + originalVersionAliasSrc, + computeOut(originalVersionAliasSrc, family, originalVersionSegment), + family + ), + } + if (strategy === 'redirect:to') { + originalVersionAlias.out = undefined + originalVersionAlias.rel = symbolicVersionAlias + return originalVersionAlias + } else { + symbolicVersionAlias.out = undefined + symbolicVersionAlias.rel = originalVersionAlias + return symbolicVersionAlias + } +} + + +function computeOut (src, family, version, htmlUrlExtensionStyle) { + let { component, module: module_, basename, extname, relative, stem } = src + if (module_ === 'ROOT') module_ = '' + let indexifyPathSegment = '' + let familyPathSegment = '' + + if (family === 'page') { + if (stem !== 'index' && htmlUrlExtensionStyle === 'indexify') { + basename = 'index.html' + indexifyPathSegment = stem + } else if (extname === '.adoc') { + basename = stem + '.html' + } + } else if (family === 'image') { + familyPathSegment = '_images' + } else if (family === 'attachment') { + familyPathSegment = '_attachments' + } + const modulePath = path.join(component, version, module_) + const dirname = path.join(modulePath, familyPathSegment, path.dirname(relative), indexifyPathSegment) + const path_ = path.join(dirname, basename) + const moduleRootPath = path.relative(dirname, modulePath) || '.' + const rootPath = path.relative(dirname, '') || '.' + + return { dirname, basename, path: path_, moduleRootPath, rootPath } +} + +function computePub (src, out, family, version, htmlUrlExtensionStyle) { + const pub = {} + let url + if (family === 'nav') { + const urlSegments = version ? [src.component, version] : [src.component] + if (src.module && src.module !== 'ROOT') urlSegments.push(src.module) + // an artificial URL used for resolving page references in navigation model + url = '/' + urlSegments.join('/') + '/' + pub.moduleRootPath = '.' + } else if (family === 'page') { + const urlSegments = out.path.split('/') + const lastUrlSegmentIdx = urlSegments.length - 1 + if (htmlUrlExtensionStyle === 'drop') { + // drop just the .html extension or, if the filename is index.html, the whole segment + const lastUrlSegment = urlSegments[lastUrlSegmentIdx] + urlSegments[lastUrlSegmentIdx] = + lastUrlSegment === 'index.html' ? '' : lastUrlSegment.substr(0, lastUrlSegment.length - 5) + } else if (htmlUrlExtensionStyle === 'indexify') { + urlSegments[lastUrlSegmentIdx] = '' + } + url = '/' + urlSegments.join('/') + } else { + url = '/' + out.path + if (family === 'alias' && !src.relative.length) pub.splat = true + } + + pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url + + if (out) { + pub.moduleRootPath = out.moduleRootPath + pub.rootPath = out.rootPath + } + + return pub +} + +function computeVersionSegment (name, version, mode) { + if (mode === 'original') return !version || version === 'master' ? '' : version + const strategy = this.latestVersionUrlSegmentStrategy + // NOTE: special exception; revisit in Antora 3 + if (!version || version === 'master') { + if (mode !== 'alias') return '' + if (strategy === 'redirect:to') return + } + if (strategy === 'redirect:to' || strategy === (mode === 'alias' ? 'redirect:from' : 'replace')) { + const component = this.getComponent(name) + const componentVersion = component && this.getComponentVersion(component, version) + if (componentVersion) { + const segment = + componentVersion === component.latest + ? this.latestVersionUrlSegment + : componentVersion === component.latestPrerelease + ? this.latestPrereleaseVersionUrlSegment + : undefined + return segment == null ? version : segment + } + } + return version +} + +function getGeneration(version) { + if (!version) return version; + const firstIndex = version.indexOf('.') + if (firstIndex < 0) { + return version; + } + const secondIndex = version.indexOf('.', firstIndex + 1); + const result = version.substr(0, secondIndex); + return result; +} + +function out(args) { + console.log(JSON.stringify(args, no_data, 2)); +} + + +function no_data(key, value) { + if (key == "data" || key == "files") { + return value ? "__data__" : value; + } + return value; +} diff --git a/docs/antora/extensions/root-component-name.js b/docs/antora/extensions/root-component-name.js new file mode 100644 index 00000000000..dcc8dc482c7 --- /dev/null +++ b/docs/antora/extensions/root-component-name.js @@ -0,0 +1,40 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + +const { posix: path } = require('path') + +module.exports.register = (pipeline, { config }) => { + pipeline.on('contentClassified', ({ contentCatalog }) => { + const rootComponentName = config.rootComponentName || 'ROOT' + const rootComponentNameLength = rootComponentName.length + contentCatalog.findBy({ component: rootComponentName }).forEach((file) => { + if (file.out) { + file.out.dirname = file.out.dirname.substr(rootComponentNameLength) + file.out.path = file.out.path.substr(rootComponentNameLength + 1) + file.out.rootPath = fixPath(file.out.rootPath) + } + if (file.pub) { + file.pub.url = file.pub.url.substr(rootComponentNameLength + 1) + if (file.pub.rootPath) { + file.pub.rootPath = fixPath(file.pub.rootPath) + } + } + if (file.rel) { + if (file.rel.pub) { + file.rel.pub.url = file.rel.pub.url.substr(rootComponentNameLength + 1) + file.rel.pub.rootPath = fixPath(file.rel.pub.rootPath); + } + } + }) + const rootComponent = contentCatalog.getComponent(rootComponentName) + rootComponent?.versions?.forEach((version) => { + version.url = version.url.substr(rootComponentName.length + 1) + }) + // const siteStartPage = contentCatalog.getById({ component: '', version: '', module: '', family: 'alias', relative: 'index.adoc' }) + // if (siteStartPage) delete siteStartPage.out + }) + + function fixPath(path) { + return path.split('/').slice(1).join('/') || '.' + } +} \ No newline at end of file diff --git a/docs/antora/extensions/version-fix.js b/docs/antora/extensions/version-fix.js new file mode 100644 index 00000000000..b0208d25d75 --- /dev/null +++ b/docs/antora/extensions/version-fix.js @@ -0,0 +1,35 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + + +module.exports.register = function({ config }) { + this.on('contentAggregated', ({ contentAggregate }) => { + contentAggregate.forEach(aggregate => { + if (aggregate.name === "" && aggregate.displayVersion === 5.6) { + aggregate.name = "ROOT"; + aggregate.version = "5.6.0-RC1" + aggregate.startPage = "ROOT:index.adoc" + aggregate.displayVersion = `${aggregate.version}` + delete aggregate.prerelease + } + if (aggregate.version === "5.6.1" && + aggregate.prerelease == "-SNAPSHOT") { + aggregate.version = "5.6.1" + aggregate.displayVersion = `${aggregate.version}` + delete aggregate.prerelease + } + }) + }) +} + +function out(args) { + console.log(JSON.stringify(args, no_data, 2)); +} + + +function no_data(key, value) { + if (key == "data" || key == "files") { + return value ? "__data__" : value; + } + return value; +} diff --git a/docs/local-antora-playbook.yml b/docs/local-antora-playbook.yml new file mode 100644 index 00000000000..dc37701f40f --- /dev/null +++ b/docs/local-antora-playbook.yml @@ -0,0 +1,26 @@ +site: + title: Spring Security + url: https://docs.spring.io/spring-security/reference/ +asciidoc: + attributes: + page-pagination: true +content: + sources: + - url: https://github.com/spring-io/spring-generated-docs + branches: [spring-projects/spring-security/5.8.x] + - url: ../ + branches: HEAD + start_path: docs +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip + snapshot: true + +antora: + extensions: + - require: ./antora/extensions/version-fix.js + - require: ./antora/extensions/major-minor-segment.js diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg new file mode 100644 index 00000000000..95247c4ba98 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg differ diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png new file mode 100644 index 00000000000..5d8b22b108a Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png differ diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextpersistencefilter.odg b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextpersistencefilter.odg new file mode 100644 index 00000000000..c4a74df3d37 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextpersistencefilter.odg differ diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextpersistencefilter.png b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextpersistencefilter.png new file mode 100644 index 00000000000..ac12c18c453 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextpersistencefilter.png differ diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.odg b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.odg new file mode 100644 index 00000000000..5ef95428f95 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.odg differ diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.png b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.png new file mode 100644 index 00000000000..8118785797c Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.png differ diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.odg b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.odg new file mode 100644 index 00000000000..bcfc3c34d42 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.odg differ diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.png b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.png new file mode 100644 index 00000000000..86dc1ac3534 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.png differ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f08028dcdf8..a4b0fb6cf4f 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -36,6 +36,7 @@ ***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder] ***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider] ***** xref:servlet/authentication/passwords/ldap.adoc[LDAP] +*** xref:servlet/authentication/persistence.adoc[Persistence] *** xref:servlet/authentication/session-management.adoc[Session Management] *** xref:servlet/authentication/rememberme.adoc[Remember Me] *** xref:servlet/authentication/openid.adoc[OpenID] @@ -49,14 +50,22 @@ *** 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] *** xref:servlet/authorization/acls.adoc[Domain Object Security ACLs] +*** xref:servlet/authorization/events.adoc[Authorization Events] ** xref:servlet/oauth2/index.adoc[OAuth2] -*** xref:servlet/oauth2/oauth2-login.adoc[OAuth2 Log In] -*** xref:servlet/oauth2/oauth2-client.adoc[OAuth2 Client] +*** xref:servlet/oauth2/login/index.adoc[OAuth2 Log In] +**** xref:servlet/oauth2/login/core.adoc[Core Configuration] +**** xref:servlet/oauth2/login/advanced.adoc[Advanced Configuration] +*** xref:servlet/oauth2/client/index.adoc[OAuth2 Client] +**** xref:servlet/oauth2/client/core.adoc[Core Interfaces and Classes] +**** xref:servlet/oauth2/client/authorization-grants.adoc[OAuth2 Authorization Grants] +**** xref:servlet/oauth2/client/client-authentication.adoc[OAuth2 Client Authentication] +**** xref:servlet/oauth2/client/authorized-clients.adoc[OAuth2 Authorized Clients] *** xref:servlet/oauth2/resource-server/index.adoc[OAuth2 Resource Server] **** xref:servlet/oauth2/resource-server/jwt.adoc[JWT] **** xref:servlet/oauth2/resource-server/opaque-token.adoc[Opaque Token] @@ -75,7 +84,11 @@ *** xref:servlet/exploits/http.adoc[] *** xref:servlet/exploits/firewall.adoc[] ** xref:servlet/integrations/index.adoc[Integrations] +*** xref:servlet/integrations/concurrency.adoc[Concurrency] +*** xref:servlet/integrations/jackson.adoc[Jackson] +*** xref:servlet/integrations/localization.adoc[Localization] *** xref:servlet/integrations/servlet-api.adoc[Servlet APIs] +*** xref:servlet/integrations/data.adoc[Spring Data] *** xref:servlet/integrations/mvc.adoc[Spring MVC] *** xref:servlet/integrations/websocket.adoc[WebSocket] *** xref:servlet/integrations/cors.adoc[Spring's CORS Support] @@ -100,7 +113,12 @@ *** xref:servlet/test/mockmvc/result-handlers.adoc[Security ResultHandlers] ** xref:servlet/appendix/index.adoc[Appendix] *** xref:servlet/appendix/database-schema.adoc[Database Schemas] -*** xref:servlet/appendix/namespace.adoc[XML Namespace] +*** xref:servlet/appendix/namespace/index.adoc[XML Namespace] +**** xref:servlet/appendix/namespace/authentication-manager.adoc[Authentication Services] +**** xref:servlet/appendix/namespace/http.adoc[Web Security] +**** xref:servlet/appendix/namespace/method-security.adoc[Method Security] +**** xref:servlet/appendix/namespace/ldap.adoc[LDAP Security] +**** xref:servlet/appendix/namespace/websocket.adoc[WebSocket Security] *** xref:servlet/appendix/faq.adoc[FAQ] * xref:reactive/index.adoc[Reactive Applications] ** xref:reactive/getting-started.adoc[Getting Started] @@ -108,10 +126,17 @@ *** 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.adoc[OAuth2 Log In] -*** xref:reactive/oauth2/oauth2-client.adoc[OAuth2 Client] +*** xref:reactive/oauth2/login/index.adoc[OAuth2 Log In] +**** xref:reactive/oauth2/login/core.adoc[Core Configuration] +**** xref:reactive/oauth2/login/advanced.adoc[Advanced Configuration] +*** xref:reactive/oauth2/client/index.adoc[OAuth2 Client] +**** xref:reactive/oauth2/client/core.adoc[Core Interfaces and Classes] +**** xref:reactive/oauth2/client/authorization-grants.adoc[OAuth2 Authorization Grants] +**** xref:reactive/oauth2/client/client-authentication.adoc[OAuth2 Client Authentication] +**** xref:reactive/oauth2/client/authorized-clients.adoc[OAuth2 Authorized Clients] *** xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server] **** xref:reactive/oauth2/resource-server/jwt.adoc[JWT] **** xref:reactive/oauth2/resource-server/opaque-token.adoc[Opaque Token] diff --git a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc index dcbf3ab8ad3..4800f29a1d3 100644 --- a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc +++ b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc @@ -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(); } ---- diff --git a/docs/modules/ROOT/pages/features/exploits/headers.adoc b/docs/modules/ROOT/pages/features/exploits/headers.adoc index be19d3028cb..e142718275d 100644 --- a/docs/modules/ROOT/pages/features/exploits/headers.adoc +++ b/docs/modules/ROOT/pages/features/exploits/headers.adoc @@ -378,6 +378,26 @@ Clear-Site-Data: "cache", "cookies", "storage", "executionContexts" This is a nice clean-up action to perform on logout. +[[headers-cross-origin-policies]] +== Cross-Origin Policies + +[NOTE] +==== +Refer to the relevant sections to see how to configure for both <> and <> based applications. +==== + +Spring Security provides support for some important Cross-Origin Policies headers. +Those headers are: + +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy[`Cross-Origin-Opener-Policy`] +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy[`Cross-Origin-Embedder-Policy`] +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy[`Cross-Origin-Resource-Policy`] + +`Cross-Origin-Opener-Policy` (COOP) allows a top-level document to break the association between its window and any others in the browsing context group (e.g., between a popup and its opener), preventing any direct DOM access between them. + +Enabling `Cross-Origin-Embedder-Policy` (COEP) prevents a document from loading any non-same-origin resources which don't explicitly grant the document permission to be loaded. + +The `Cross-Origin-Resource-Policy` (CORP) header allows you to control the set of origins that are empowered to include a resource. It is a robust defense against attacks like https://meltdownattack.com[Spectre], as it allows browsers to block a given response before it enters an attacker's process. [[headers-custom]] == Custom Headers diff --git a/docs/modules/ROOT/pages/features/integrations/concurrency.adoc b/docs/modules/ROOT/pages/features/integrations/concurrency.adoc index fbb049c9e99..69c5978bcb8 100644 --- a/docs/modules/ROOT/pages/features/integrations/concurrency.adoc +++ b/docs/modules/ROOT/pages/features/integrations/concurrency.adoc @@ -44,7 +44,7 @@ fun run() { While very simple, it makes it seamless to transfer the SecurityContext from one Thread to another. This is important since, in most cases, the SecurityContextHolder acts on a per Thread basis. -For example, you might have used Spring Security's xref:servlet/appendix/namespace.adoc#nsa-global-method-security[] support to secure one of your services. +For example, you might have used Spring Security's xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[] support to secure one of your services. You can now easily transfer the `SecurityContext` of the current `Thread` to the `Thread` that invokes the secured service. An example of how you might do this can be found below: @@ -137,7 +137,7 @@ You can see an example of how it might be used below: ---- SecurityContext context = SecurityContextHolder.createEmptyContext(); Authentication authentication = - new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER")); + UsernamePasswordAuthenticationToken.authenticated("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER")); context.setAuthentication(authentication); SimpleAsyncTaskExecutor delegateExecutor = diff --git a/docs/modules/ROOT/pages/features/integrations/cryptography.adoc b/docs/modules/ROOT/pages/features/integrations/cryptography.adoc index 137b3e8b694..c91c882d0a8 100644 --- a/docs/modules/ROOT/pages/features/integrations/cryptography.adoc +++ b/docs/modules/ROOT/pages/features/integrations/cryptography.adoc @@ -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; + } } ---- diff --git a/docs/modules/ROOT/pages/features/integrations/jackson.adoc b/docs/modules/ROOT/pages/features/integrations/jackson.adoc index e1c407763b3..1f67bf98a20 100644 --- a/docs/modules/ROOT/pages/features/integrations/jackson.adoc +++ b/docs/modules/ROOT/pages/features/integrations/jackson.adoc @@ -42,6 +42,6 @@ The following Spring Security modules provide Jackson support: - spring-security-core (`CoreJackson2Module`) - spring-security-web (`WebJackson2Module`, `WebServletJackson2Module`, `WebServerJackson2Module`) -- xref:servlet/oauth2/oauth2-client.adoc#oauth2client[ spring-security-oauth2-client] (`OAuth2ClientJackson2Module`) +- xref:servlet/oauth2/client/index.adoc#oauth2client[ spring-security-oauth2-client] (`OAuth2ClientJackson2Module`) - spring-security-cas (`CasJackson2Module`) ==== diff --git a/docs/modules/ROOT/pages/getting-spring-security.adoc b/docs/modules/ROOT/pages/getting-spring-security.adoc index f99e18860fd..7b2aa7e357a 100644 --- a/docs/modules/ROOT/pages/getting-spring-security.adoc +++ b/docs/modules/ROOT/pages/getting-spring-security.adoc @@ -53,7 +53,7 @@ If you wish to override the Spring Security version, you may do so by providing {spring-security-version} - + ---- ==== @@ -68,7 +68,7 @@ You can do so by adding a Maven property, as the following example shows: {spring-core-version} - + ---- ==== diff --git a/docs/modules/ROOT/pages/reactive/authentication/logout.adoc b/docs/modules/ROOT/pages/reactive/authentication/logout.adoc index 0d4137d1081..d4dafaa9c3b 100644 --- a/docs/modules/ROOT/pages/reactive/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/reactive/authentication/logout.adoc @@ -11,7 +11,8 @@ This will: Often, you will want to also invalidate the session on logout. To achieve this, you can add the `WebSessionServerLogoutHandler` to your logout configuration, like so: -[source,java] +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception { @@ -26,3 +27,23 @@ SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception { return http.build(); } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun http(http: ServerHttpSecurity): SecurityWebFilterChain { + val customLogoutHandler = DelegatingServerLogoutHandler( + WebSessionServerLogoutHandler(), SecurityContextServerLogoutHandler() + ) + + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + logout { + logoutHandler = customLogoutHandler + } + } +} +---- diff --git a/docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc new file mode 100644 index 00000000000..2a4faefc655 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc @@ -0,0 +1,104 @@ +[[servlet-authorization-authorizationfilter]] += Authorize ServerHttpRequest + +Spring Security provides support for authorizing the incoming HTTP requests. +By default, Spring Security’s 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. diff --git a/docs/modules/ROOT/pages/reactive/exploits/headers.adoc b/docs/modules/ROOT/pages/reactive/exploits/headers.adoc index 9b61d12b3aa..30b4779fd92 100644 --- a/docs/modules/ROOT/pages/reactive/exploits/headers.adoc +++ b/docs/modules/ROOT/pages/reactive/exploits/headers.adoc @@ -578,3 +578,65 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { } ---- ==== + +[[webflux-headers-cross-origin-policies]] +== Cross-Origin Policies + +Spring Security provides built-in support for adding some Cross-Origin policies headers, those headers are: + +[source] +---- +Cross-Origin-Opener-Policy +Cross-Origin-Embedder-Policy +Cross-Origin-Resource-Policy +---- + +Spring Security does not add <> headers by default. +The headers can be added with the following configuration: + +.Cross-Origin Policies +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +@EnableWebFlux +public class WebSecurityConfig { + + @Bean + SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { + http.headers((headers) -> headers + .crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + .crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + .crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN)); + return http.build(); + } +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +@EnableWebFlux +open class CrossOriginPoliciesCustomConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN) + } + } + } +} +---- +==== + +This configuration will write the headers with the values provided: +[source] +---- +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: same-origin +---- diff --git a/docs/modules/ROOT/pages/reactive/integrations/rsocket.adoc b/docs/modules/ROOT/pages/reactive/integrations/rsocket.adoc index eed51eb691e..924cdbacda7 100644 --- a/docs/modules/ROOT/pages/reactive/integrations/rsocket.adoc +++ b/docs/modules/ROOT/pages/reactive/integrations/rsocket.adoc @@ -61,7 +61,9 @@ For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure. In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code. -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) { @@ -69,6 +71,20 @@ RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInte } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor): RSocketServerCustomizer { + return RSocketServerCustomizer { server -> + server.interceptors { registry -> + registry.forSocketAcceptor(interceptor) + } + } +} +---- +==== + [[rsocket-authentication]] == RSocket Authentication diff --git a/docs/modules/ROOT/pages/reactive/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc similarity index 52% rename from docs/modules/ROOT/pages/reactive/oauth2/oauth2-client.adoc rename to docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc index a44e3312b39..6e42dcb6982 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc @@ -1,596 +1,21 @@ -[[webflux-oauth2-client]] -= OAuth 2.0 Client - -The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. - -At a high-level, the core features available are: - -.Authorization Grant support -* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] -* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] -* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] -* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] - -.Client Authentication support -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] - -.HTTP Client support -* <> (for requesting protected resources) - -The `ServerHttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. - -The following code shows the complete configuration options provided by the `ServerHttpSecurity.oauth2Client()` DSL: - -.OAuth2 Client Configuration Options -==== -.Java -[source,java,role="primary"] ----- -@EnableWebFluxSecurity -public class OAuth2ClientSecurityConfig { - - @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .oauth2Client(oauth2 -> oauth2 - .clientRegistrationRepository(this.clientRegistrationRepository()) - .authorizedClientRepository(this.authorizedClientRepository()) - .authorizationRequestRepository(this.authorizationRequestRepository()) - .authenticationConverter(this.authenticationConverter()) - .authenticationManager(this.authenticationManager()) - ); - - return http.build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebFluxSecurity -class OAuth2ClientSecurityConfig { - - @Bean - fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - oauth2Client { - clientRegistrationRepository = clientRegistrationRepository() - authorizedClientRepository = authorizedClientRepository() - authorizationRequestRepository = authorizedRequestRepository() - authenticationConverter = authenticationConverter() - authenticationManager = authenticationManager() - } - } - - return http.build() - } -} ----- -==== - -The `ReactiveOAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `ReactiveOAuth2AuthorizedClientProvider`(s). - -The following code shows an example of how to register a `ReactiveOAuth2AuthorizedClientManager` `@Bean` and associate it with a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -The following sections will go into more detail on the core components used by OAuth 2.0 Client and the configuration options available: - -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -* <> -** <> -* <> - - -[[oauth2Client-core-interface-class]] -== Core Interfaces / Classes - - -[[oauth2Client-client-registration]] -=== ClientRegistration - -`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - -A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. - -`ClientRegistration` and its properties are defined as follows: - -[source,java] ----- -public final class ClientRegistration { - private String registrationId; <1> - private String clientId; <2> - private String clientSecret; <3> - private ClientAuthenticationMethod clientAuthenticationMethod; <4> - private AuthorizationGrantType authorizationGrantType; <5> - private String redirectUri; <6> - private Set scopes; <7> - private ProviderDetails providerDetails; - private String clientName; <8> - - public class ProviderDetails { - private String authorizationUri; <9> - private String tokenUri; <10> - private UserInfoEndpoint userInfoEndpoint; - private String jwkSetUri; <11> - private String issuerUri; <12> - private Map configurationMetadata; <13> - - public class UserInfoEndpoint { - private String uri; <14> - private AuthenticationMethod authenticationMethod; <15> - private String userNameAttributeName; <16> - - } - } -} ----- -<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. -<2> `clientId`: The client identifier. -<3> `clientSecret`: The client secret. -<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. -The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. -<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. - The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. -<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent - to after the end-user has authenticated and authorized access to the client. -<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. -<8> `clientName`: A descriptive name used for the client. -The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. -<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. -<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. -<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, - which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. -<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. -<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. - This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. -<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. -<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. -The supported values are *header*, *form* and *query*. -<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. - -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. - -`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: - -==== -.Java -[source,java,role="primary"] ----- -ClientRegistration clientRegistration = - ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() ----- -==== - -The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. - -As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. - -[[oauth2Client-client-registration-repo]] -=== ReactiveClientRegistrationRepository - -The `ReactiveClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). - -[NOTE] -Client registration information is ultimately stored and owned by the associated Authorization Server. -This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. - -Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ReactiveClientRegistrationRepository`. - -[NOTE] -The default implementation of `ReactiveClientRegistrationRepository` is `InMemoryReactiveClientRegistrationRepository`. - -The auto-configuration also registers the `ReactiveClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private ReactiveClientRegistrationRepository clientRegistrationRepository; - - @GetMapping("/") - public Mono index() { - return this.clientRegistrationRepository.findByRegistrationId("okta") - ... - .thenReturn("index"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository - - @GetMapping("/") - fun index(): Mono { - return this.clientRegistrationRepository.findByRegistrationId("okta") - ... - .thenReturn("index") - } -} ----- -==== - -[[oauth2Client-authorized-client]] -=== OAuth2AuthorizedClient - -`OAuth2AuthorizedClient` is a representation of an Authorized Client. -A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. - -`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. - - -[[oauth2Client-authorized-repo-service]] -=== ServerOAuth2AuthorizedClientRepository / ReactiveOAuth2AuthorizedClientService - -`ServerOAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. -Whereas, the primary role of `ReactiveOAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. - -From a developer perspective, the `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private ReactiveOAuth2AuthorizedClientService authorizedClientService; - - @GetMapping("/") - public Mono index(Authentication authentication) { - return this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()) - .map(OAuth2AuthorizedClient::getAccessToken) - ... - .thenReturn("index"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var authorizedClientService: ReactiveOAuth2AuthorizedClientService - - @GetMapping("/") - fun index(authentication: Authentication): Mono { - return this.authorizedClientService.loadAuthorizedClient("okta", authentication.name) - .map { it.accessToken } - ... - .thenReturn("index") - } -} ----- -==== - -[NOTE] -Spring Boot 2.x auto-configuration registers an `ServerOAuth2AuthorizedClientRepository` and/or `ReactiveOAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. -However, the application may choose to override and register a custom `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` `@Bean`. - -The default implementation of `ReactiveOAuth2AuthorizedClientService` is `InMemoryReactiveOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. - -Alternatively, the R2DBC implementation `R2dbcReactiveOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. - -[NOTE] -`R2dbcReactiveOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. - - -[[oauth2Client-authorized-manager-provider]] -=== ReactiveOAuth2AuthorizedClientManager / ReactiveOAuth2AuthorizedClientProvider - -The `ReactiveOAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). - -The primary responsibilities include: - -* Authorizing (or re-authorizing) an OAuth 2.0 Client, using a `ReactiveOAuth2AuthorizedClientProvider`. -* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using a `ReactiveOAuth2AuthorizedClientService` or `ServerOAuth2AuthorizedClientRepository`. -* Delegating to a `ReactiveOAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). -* Delegating to a `ReactiveOAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). - -A `ReactiveOAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. -Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. - -The default implementation of `ReactiveOAuth2AuthorizedClientManager` is `DefaultReactiveOAuth2AuthorizedClientManager`, which is associated with a `ReactiveOAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. -The `ReactiveOAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. - -The following code shows an example of how to configure and build a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -When an authorization attempt succeeds, the `DefaultReactiveOAuth2AuthorizedClientManager` will delegate to the `ReactiveOAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `ServerOAuth2AuthorizedClientRepository`. -In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `ServerOAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler`. -The default behaviour may be customized via `setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)`. - -The `DefaultReactiveOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. -This can be useful when you need to supply a `ReactiveOAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordReactiveOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. - -The following code shows an example of the `contextAttributesMapper`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build(); - - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, - // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); - - return authorizedClientManager; -} - -private Function>> contextAttributesMapper() { - return authorizeRequest -> { - Map contextAttributes = Collections.emptyMap(); - ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); - ServerHttpRequest request = exchange.getRequest(); - String username = request.getQueryParams().getFirst(OAuth2ParameterNames.USERNAME); - String password = request.getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = new HashMap<>(); - - // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); - contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); - } - return Mono.just(contextAttributes); - }; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build() - val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - - // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, - // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) - return authorizedClientManager -} - -private fun contextAttributesMapper(): Function>> { - return Function { authorizeRequest -> - var contextAttributes: MutableMap = mutableMapOf() - val exchange: ServerWebExchange = authorizeRequest.getAttribute(ServerWebExchange::class.java.name)!! - val request: ServerHttpRequest = exchange.request - val username: String? = request.queryParams.getFirst(OAuth2ParameterNames.USERNAME) - val password: String? = request.queryParams.getFirst(OAuth2ParameterNames.PASSWORD) - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = hashMapOf() - - // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username!! - contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password!! - } - Mono.just(contextAttributes) - } -} ----- -==== - -The `DefaultReactiveOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `ServerWebExchange`. -When operating *_outside_* of a `ServerWebExchange` context, use `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` instead. - -A _service application_ is a common use case for when to use an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`. -Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. -An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. - -The following code shows an example of how to configure an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ReactiveOAuth2AuthorizedClientService authorizedClientService) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build(); - - AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientService: ReactiveOAuth2AuthorizedClientService): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build() - val authorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - - [[oauth2Client-auth-grant-support]] -== Authorization Grant Support += Authorization Grant Support [[oauth2Client-auth-code-grant]] -=== Authorization Code +== Authorization Code [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant. -==== Obtaining Authorization +=== Obtaining Authorization [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.1.1[Authorization Request/Response] protocol flow for the Authorization Code grant. -==== Initiating the Authorization Request +=== Initiating the Authorization Request The `OAuth2AuthorizationRequestRedirectWebFilter` uses a `ServerOAuth2AuthorizationRequestResolver` to resolve an `OAuth2AuthorizationRequest` and initiate the Authorization Code grant flow by redirecting the end-user's user-agent to the Authorization Server's Authorization Endpoint. @@ -647,6 +72,9 @@ If the client is running in an untrusted environment (eg. native application or . `client-secret` is omitted (or empty) . `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`) +[TIP] +If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultServerOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`. + [[oauth2Client-auth-code-redirect-uri]] The `DefaultServerOAuth2AuthorizationRequestResolver` also supports `URI` template variables for the `redirect-uri` using `UriComponentsBuilder`. @@ -671,7 +99,7 @@ spring: Configuring the `redirect-uri` with `URI` template variables is especially useful when the OAuth 2.0 Client is running behind a xref:features/exploits/http.adoc#http-proxy-server[Proxy Server]. This ensures that the `X-Forwarded-*` headers are used when expanding the `redirect-uri`. -==== Customizing the Authorization Request +=== Customizing the Authorization Request One of the primary use cases a `ServerOAuth2AuthorizationRequestResolver` can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework. @@ -737,7 +165,7 @@ class SecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } @@ -745,8 +173,6 @@ class SecurityConfig { authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository) } } - - return http.build() } private fun authorizationRequestResolver( @@ -818,7 +244,7 @@ private fun authorizationRequestCustomizer(): Consumer>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.1.3[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Authorization Code grant are added directly to the body of the request by the `WebClientReactiveAuthorizationCodeTokenResponseClient`. @@ -891,12 +315,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveAuthorizationCodeTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveAuthorizationCodeTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -938,13 +362,11 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { authenticationManager = authorizationCodeAuthenticationManager() } } - - return http.build() } private fun authorizationCodeAuthenticationManager(): ReactiveAuthenticationManager { @@ -959,13 +381,13 @@ class OAuth2ClientSecurityConfig { [[oauth2Client-refresh-token-grant]] -=== Refresh Token +== Refresh Token [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.5[Refresh Token]. -==== Refreshing an Access Token +=== Refreshing an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-6[Access Token Request/Response] protocol flow for the Refresh Token grant. @@ -975,7 +397,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactiveRefreshTokenTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveRefreshTokenTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-6[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Refresh Token grant are added directly to the body of the request by the `WebClientReactiveRefreshTokenTokenResponseClient`. @@ -987,12 +409,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveRefreshTokenTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveRefreshTokenTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1043,13 +465,13 @@ If the `OAuth2AuthorizedClient.getRefreshToken()` is available and the `OAuth2Au [[oauth2Client-client-creds-grant]] -=== Client Credentials +== Client Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.4.2[Access Token Request/Response] protocol flow for the Client Credentials grant. @@ -1059,7 +481,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactiveClientCredentialsTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveClientCredentialsTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Client Credentials grant are added directly to the body of the request by the `WebClientReactiveClientCredentialsTokenResponseClient`. @@ -1071,12 +493,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveClientCredentialsTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveClientCredentialsTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1119,7 +541,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()` configures a `ClientCredentialsReactiveOAuth2AuthorizedClientProvider`, which is an implementation of a `ReactiveOAuth2AuthorizedClientProvider` for the Client Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1240,13 +662,13 @@ If not provided, it will be obtained from the https://projectreactor.io/docs/cor [[oauth2Client-password-grant]] -=== Resource Owner Password Credentials +== Resource Owner Password Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.3.2[Access Token Request/Response] protocol flow for the Resource Owner Password Credentials grant. @@ -1256,7 +678,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactivePasswordTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactivePasswordTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Resource Owner Password Credentials grant are added directly to the body of the request by the `WebClientReactivePasswordTokenResponseClient`. @@ -1268,12 +690,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactivePasswordTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactivePasswordTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1317,7 +739,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `ReactiveOAuth2AuthorizedClientProviderBuilder.builder().password()` configures a `PasswordReactiveOAuth2AuthorizedClientProvider`, which is an implementation of a `ReactiveOAuth2AuthorizedClientProvider` for the Resource Owner Password Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1483,13 +905,13 @@ If not provided, it will be obtained from the https://projectreactor.io/docs/cor [[oauth2Client-jwt-bearer-grant]] -=== JWT Bearer +== JWT Bearer [NOTE] Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on the https://datatracker.ietf.org/doc/html/rfc7523[JWT Bearer] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[Access Token Request/Response] protocol flow for the JWT Bearer grant. @@ -1499,7 +921,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactiveJwtBearerTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveJwtBearerTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the JWT Bearer grant are added directly to the body of the request by the `WebClientReactiveJwtBearerTokenResponseClient`. @@ -1510,12 +932,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveJwtBearerTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveJwtBearerTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1560,7 +982,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) ---- ==== -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1674,407 +1096,8 @@ class OAuth2ResourceServerController { ---- ==== - -[[oauth2Client-client-auth-support]] -== Client Authentication Support - - -[[oauth2Client-jwt-bearer-auth]] -=== JWT Bearer - [NOTE] -Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. - -The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, -which is a `Converter` that customizes the Token Request parameters by adding -a signed JSON Web Token (JWS) in the `client_assertion` parameter. - -The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS -is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. - - -==== Authenticate using `private_key_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-authentication-method: private_key_jwt - authorization-grant-type: authorization_code - ... ----- - -The following example shows how to configure `WebClientReactiveAuthorizationCodeTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - RSAPublicKey publicKey = ... - RSAPrivateKey privateKey = ... - return new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = - new WebClientReactiveAuthorizationCodeTokenResponseClient(); -tokenResponseClient.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver: Function = - Function { clientRegistration -> - if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - var publicKey: RSAPublicKey = ... - var privateKey: RSAPrivateKey = ... - RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build() - } - null - } - -val tokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient() -tokenResponseClient.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) ----- -==== - - -==== Authenticate using `client_secret_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - client-authentication-method: client_secret_jwt - authorization-grant-type: client_credentials - ... ----- - -The following example shows how to configure `WebClientReactiveClientCredentialsTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { - SecretKeySpec secretKey = new SecretKeySpec( - clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), - "HmacSHA256"); - return new OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = - new WebClientReactiveClientCredentialsTokenResponseClient(); -tokenResponseClient.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver = Function { clientRegistration: ClientRegistration -> - if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { - val secretKey = SecretKeySpec( - clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), - "HmacSHA256" - ) - OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build() - } - null -} - -val tokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient() -tokenResponseClient.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) ----- -==== - - -[[oauth2Client-additional-features]] -== Additional Features - - -[[oauth2Client-registered-authorized-client]] -=== Resolving an Authorized Client - -The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. -This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `ReactiveOAuth2AuthorizedClientManager` or `ReactiveOAuth2AuthorizedClientService`. - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @GetMapping("/") - public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - return Mono.just(authorizedClient.getAccessToken()) - ... - .thenReturn("index"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - @GetMapping("/") - fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { - return Mono.just(authorizedClient.accessToken) - ... - .thenReturn("index") - } -} ----- -==== - -The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses a <> and therefore inherits it's capabilities. - - -[[oauth2Client-webclient-webflux]] -== WebClient integration for Reactive Environments - -The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. - -The `ServerOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. -It directly uses an <> and therefore inherits the following capabilities: - -* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. -** `authorization_code` - triggers the Authorization Request redirect to initiate the flow -** `client_credentials` - the access token is obtained directly from the Token Endpoint -** `password` - the access token is obtained directly from the Token Endpoint -* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if a `ReactiveOAuth2AuthorizedClientProvider` is available to perform the authorization - -The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { - ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .filter(oauth2Client) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { - val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .filter(oauth2Client) - .build() -} ----- -==== - -=== Providing the Authorized Client - -The `ServerOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). - -The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - String resourceUri = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono(String.class) - ... - .thenReturn("index"); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { - val resourceUri: String = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono() - ... - .thenReturn("index") -} ----- -==== - -<1> `oauth2AuthorizedClient()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. - -The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public Mono index() { - String resourceUri = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono(String.class) - ... - .thenReturn("index"); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(): Mono { - val resourceUri: String = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono() - ... - .thenReturn("index") -} ----- -==== -<1> `clientRegistrationId()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. - - -=== Defaulting the Authorized Client - -If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServerOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. - -If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `ServerHttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { - ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultOAuth2AuthorizedClient(true); - return WebClient.builder() - .filter(oauth2Client) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { - val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultOAuth2AuthorizedClient(true) - return WebClient.builder() - .filter(oauth2Client) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. - -Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { - ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultClientRegistrationId("okta"); - return WebClient.builder() - .filter(oauth2Client) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { - val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultClientRegistrationId("okta") - return WebClient.builder() - .filter(oauth2Client) - .build() -} ----- -==== +`JwtBearerReactiveOAuth2AuthorizedClientProvider` resolves the `Jwt` assertion via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example. -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. +[TIP] +If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerReactiveOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function>`. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc new file mode 100644 index 00000000000..ef42bab6a1f --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc @@ -0,0 +1,250 @@ +[[oauth2Client-additional-features]] += Authorized Clients + + +[[oauth2Client-registered-authorized-client]] +== Resolving an Authorized Client + +The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. +This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `ReactiveOAuth2AuthorizedClientManager` or `ReactiveOAuth2AuthorizedClientService`. + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @GetMapping("/") + public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + return Mono.just(authorizedClient.getAccessToken()) + ... + .thenReturn("index"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + @GetMapping("/") + fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { + return Mono.just(authorizedClient.accessToken) + ... + .thenReturn("index") + } +} +---- +==== + +The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses a <> and therefore inherits it's capabilities. + + +[[oauth2Client-webclient-webflux]] +== WebClient integration for Reactive Environments + +The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. + +The `ServerOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. +It directly uses an <> and therefore inherits the following capabilities: + +* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. +** `authorization_code` - triggers the Authorization Request redirect to initiate the flow +** `client_credentials` - the access token is obtained directly from the Token Endpoint +** `password` - the access token is obtained directly from the Token Endpoint +* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if a `ReactiveOAuth2AuthorizedClientProvider` is available to perform the authorization + +The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + return WebClient.builder() + .filter(oauth2Client) + .build() +} +---- +==== + +=== Providing the Authorized Client + +The `ServerOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). + +The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + String resourceUri = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono(String.class) + ... + .thenReturn("index"); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { + val resourceUri: String = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono() + ... + .thenReturn("index") +} +---- +==== + +<1> `oauth2AuthorizedClient()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. + +The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public Mono index() { + String resourceUri = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono(String.class) + ... + .thenReturn("index"); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(): Mono { + val resourceUri: String = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono() + ... + .thenReturn("index") +} +---- +==== +<1> `clientRegistrationId()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. + + +=== Defaulting the Authorized Client + +If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServerOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. + +If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `ServerHttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultOAuth2AuthorizedClient(true); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultOAuth2AuthorizedClient(true) + return WebClient.builder() + .filter(oauth2Client) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. + +Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultClientRegistrationId("okta"); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId("okta") + return WebClient.builder() + .filter(oauth2Client) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc new file mode 100644 index 00000000000..4840edcb1c0 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc @@ -0,0 +1,183 @@ +[[oauth2Client-client-auth-support]] += Client Authentication Support + + +[[oauth2Client-jwt-bearer-auth]] +== JWT Bearer + +[NOTE] +Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. + +The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, +which is a `Converter` that customizes the Token Request parameters by adding +a signed JSON Web Token (JWS) in the `client_assertion` parameter. + +The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS +is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. + + +=== Authenticate using `private_key_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-authentication-method: private_key_jwt + authorization-grant-type: authorization_code + ... +---- + +The following example shows how to configure `WebClientReactiveAuthorizationCodeTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + RSAPublicKey publicKey = ... + RSAPrivateKey privateKey = ... + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = + new WebClientReactiveAuthorizationCodeTokenResponseClient(); +tokenResponseClient.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver: Function = + Function { clientRegistration -> + if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + var publicKey: RSAPublicKey = ... + var privateKey: RSAPrivateKey = ... + RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null + } + +val tokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient() +tokenResponseClient.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) +---- +==== + + +=== Authenticate using `client_secret_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + client-authentication-method: client_secret_jwt + authorization-grant-type: client_credentials + ... +---- + +The following example shows how to configure `WebClientReactiveClientCredentialsTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + SecretKeySpec secretKey = new SecretKeySpec( + clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), + "HmacSHA256"); + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = + new WebClientReactiveClientCredentialsTokenResponseClient(); +tokenResponseClient.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver = Function { clientRegistration: ClientRegistration -> + if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { + val secretKey = SecretKeySpec( + clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), + "HmacSHA256" + ) + OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null +} + +val tokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient() +tokenResponseClient.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) +---- +==== + +=== Customizing the JWT assertion + +The JWT produced by `NimbusJwtClientAuthenticationParametersConverter` contains the `iss`, `sub`, `aud`, `jti`, `iat` and `exp` claims by default. You can customize the headers and/or claims by providing a `Consumer>` to `setJwtClientAssertionCustomizer()`. The following example shows how to customize claims of the JWT: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = ... + +NimbusJwtClientAuthenticationParametersConverter converter = + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver); +converter.setJwtClientAssertionCustomizer((context) -> { + context.getHeaders().header("custom-header", "header-value"); + context.getClaims().claim("custom-claim", "claim-value"); +}); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver = ... + +val converter: NimbusJwtClientAuthenticationParametersConverter = + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +converter.setJwtClientAssertionCustomizer { context -> + context.headers.header("custom-header", "header-value") + context.claims.claim("custom-claim", "claim-value") +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc new file mode 100644 index 00000000000..95ce3fd7c6a --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc @@ -0,0 +1,429 @@ +[[oauth2Client-core-interface-class]] += Core Interfaces / Classes + + +[[oauth2Client-client-registration]] +== ClientRegistration + +`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + +A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. + +`ClientRegistration` and its properties are defined as follows: + +[source,java] +---- +public final class ClientRegistration { + private String registrationId; <1> + private String clientId; <2> + private String clientSecret; <3> + private ClientAuthenticationMethod clientAuthenticationMethod; <4> + private AuthorizationGrantType authorizationGrantType; <5> + private String redirectUri; <6> + private Set scopes; <7> + private ProviderDetails providerDetails; + private String clientName; <8> + + public class ProviderDetails { + private String authorizationUri; <9> + private String tokenUri; <10> + private UserInfoEndpoint userInfoEndpoint; + private String jwkSetUri; <11> + private String issuerUri; <12> + private Map configurationMetadata; <13> + + public class UserInfoEndpoint { + private String uri; <14> + private AuthenticationMethod authenticationMethod; <15> + private String userNameAttributeName; <16> + + } + } +} +---- +<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. +<2> `clientId`: The client identifier. +<3> `clientSecret`: The client secret. +<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. +The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. +<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. + The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. +<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent + to after the end-user has authenticated and authorized access to the client. +<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. +<8> `clientName`: A descriptive name used for the client. +The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. +<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. +<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. +<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, + which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. +<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. +<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. + This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. +<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. +<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. +The supported values are *header*, *form* and *query*. +<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. + +`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: + +==== +.Java +[source,java,role="primary"] +---- +ClientRegistration clientRegistration = + ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() +---- +==== + +The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. + +As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. + +[[oauth2Client-client-registration-repo]] +== ReactiveClientRegistrationRepository + +The `ReactiveClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). + +[NOTE] +Client registration information is ultimately stored and owned by the associated Authorization Server. +This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. + +Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ReactiveClientRegistrationRepository`. + +[NOTE] +The default implementation of `ReactiveClientRegistrationRepository` is `InMemoryReactiveClientRegistrationRepository`. + +The auto-configuration also registers the `ReactiveClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @GetMapping("/") + public Mono index() { + return this.clientRegistrationRepository.findByRegistrationId("okta") + ... + .thenReturn("index"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @GetMapping("/") + fun index(): Mono { + return this.clientRegistrationRepository.findByRegistrationId("okta") + ... + .thenReturn("index") + } +} +---- +==== + +[[oauth2Client-authorized-client]] +== OAuth2AuthorizedClient + +`OAuth2AuthorizedClient` is a representation of an Authorized Client. +A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. + +`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. + + +[[oauth2Client-authorized-repo-service]] +== ServerOAuth2AuthorizedClientRepository / ReactiveOAuth2AuthorizedClientService + +`ServerOAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. +Whereas, the primary role of `ReactiveOAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. + +From a developer perspective, the `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @GetMapping("/") + public Mono index(Authentication authentication) { + return this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()) + .map(OAuth2AuthorizedClient::getAccessToken) + ... + .thenReturn("index"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientService: ReactiveOAuth2AuthorizedClientService + + @GetMapping("/") + fun index(authentication: Authentication): Mono { + return this.authorizedClientService.loadAuthorizedClient("okta", authentication.name) + .map { it.accessToken } + ... + .thenReturn("index") + } +} +---- +==== + +[NOTE] +Spring Boot 2.x auto-configuration registers an `ServerOAuth2AuthorizedClientRepository` and/or `ReactiveOAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. +However, the application may choose to override and register a custom `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` `@Bean`. + +The default implementation of `ReactiveOAuth2AuthorizedClientService` is `InMemoryReactiveOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. + +Alternatively, the R2DBC implementation `R2dbcReactiveOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. + +[NOTE] +`R2dbcReactiveOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. + + +[[oauth2Client-authorized-manager-provider]] +== ReactiveOAuth2AuthorizedClientManager / ReactiveOAuth2AuthorizedClientProvider + +The `ReactiveOAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). + +The primary responsibilities include: + +* Authorizing (or re-authorizing) an OAuth 2.0 Client, using a `ReactiveOAuth2AuthorizedClientProvider`. +* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using a `ReactiveOAuth2AuthorizedClientService` or `ServerOAuth2AuthorizedClientRepository`. +* Delegating to a `ReactiveOAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). +* Delegating to a `ReactiveOAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). + +A `ReactiveOAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. +Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. + +The default implementation of `ReactiveOAuth2AuthorizedClientManager` is `DefaultReactiveOAuth2AuthorizedClientManager`, which is associated with a `ReactiveOAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. +The `ReactiveOAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. + +The following code shows an example of how to configure and build a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + +When an authorization attempt succeeds, the `DefaultReactiveOAuth2AuthorizedClientManager` will delegate to the `ReactiveOAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `ServerOAuth2AuthorizedClientRepository`. +In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `ServerOAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler`. +The default behaviour may be customized via `setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)`. + +The `DefaultReactiveOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. +This can be useful when you need to supply a `ReactiveOAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordReactiveOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. + +The following code shows an example of the `contextAttributesMapper`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); + + return authorizedClientManager; +} + +private Function>> contextAttributesMapper() { + return authorizeRequest -> { + Map contextAttributes = Collections.emptyMap(); + ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); + ServerHttpRequest request = exchange.getRequest(); + String username = request.getQueryParams().getFirst(OAuth2ParameterNames.USERNAME); + String password = request.getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = new HashMap<>(); + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + return Mono.just(contextAttributes); + }; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function>> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val exchange: ServerWebExchange = authorizeRequest.getAttribute(ServerWebExchange::class.java.name)!! + val request: ServerHttpRequest = exchange.request + val username: String? = request.queryParams.getFirst(OAuth2ParameterNames.USERNAME) + val password: String? = request.queryParams.getFirst(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username!! + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password!! + } + Mono.just(contextAttributes) + } +} +---- +==== + +The `DefaultReactiveOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `ServerWebExchange`. +When operating *_outside_* of a `ServerWebExchange` context, use `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` instead. + +A _service application_ is a common use case for when to use an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`. +Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. +An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. + +The following code shows an example of how to configure an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientService: ReactiveOAuth2AuthorizedClientService): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc new file mode 100644 index 00000000000..614a3e45faa --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc @@ -0,0 +1,121 @@ +[[webflux-oauth2-client]] += OAuth 2.0 Client +:page-section-summary-toc: 1 + +The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. + +At a high-level, the core features available are: + +.Authorization Grant support +* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] +* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] +* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] +* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] + +.Client Authentication support +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] + +.HTTP Client support +* <> (for requesting protected resources) + +The `ServerHttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. + +The following code shows the complete configuration options provided by the `ServerHttpSecurity.oauth2Client()` DSL: + +.OAuth2 Client Configuration Options +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2ClientSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Client(oauth2 -> oauth2 + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizationRequestRepository(this.authorizationRequestRepository()) + .authenticationConverter(this.authenticationConverter()) + .authenticationManager(this.authenticationManager()) + ); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2ClientSecurityConfig { + + @Bean + fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizationRequestRepository = authorizedRequestRepository() + authenticationConverter = authenticationConverter() + authenticationManager = authenticationManager() + } + } + } +} +---- +==== + +The `ReactiveOAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `ReactiveOAuth2AuthorizedClientProvider`(s). + +The following code shows an example of how to register a `ReactiveOAuth2AuthorizedClientManager` `@Bean` and associate it with a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc index af06df5136b..592bc9fbab7 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc @@ -3,6 +3,6 @@ Spring Security provides OAuth2 and WebFlux integration for reactive applications. -* xref:reactive/oauth2/login.adoc[OAuth2 Log In] - Authenticating with an OAuth2 or OpenID Connect 1.0 Provider -* xref:reactive/oauth2/oauth2-client.adoc[OAuth2 Client] - Making requests to an OAuth2 Resource Server +* xref:reactive/oauth2/login/index.adoc[OAuth2 Log In] - Authenticating with an OAuth2 or OpenID Connect 1.0 Provider +* xref:reactive/oauth2/client/index.adoc[OAuth2 Client] - Making requests to an OAuth2 Resource Server * xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server] - Protecting a REST endpoint using OAuth2 diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login.adoc deleted file mode 100644 index a16160c0ff7..00000000000 --- a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc +++ /dev/null @@ -1,245 +0,0 @@ -[[webflux-oauth2-login]] -= OAuth 2.0 Login - -The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. -GitHub) or OpenID Connect 1.0 Provider (such as Google). -OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". - -NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. - -[[webflux-oauth2-login-sample]] -== Spring Boot 2.0 Sample - -Spring Boot 2.0 brings full auto-configuration capabilities for OAuth 2.0 Login. - -This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: - -* <> -* <> -* <> -* <> - - -[[webflux-oauth2-login-sample-setup]] -=== Initial setup - -To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. - -NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. - -Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". - -After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. - -[[webflux-oauth2-login-sample-redirect]] -=== Setting the redirect URI - -The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. - -In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. - -TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]. -For our example, the `registrationId` is `google`. - -IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. - -[[webflux-oauth2-login-sample-config]] -=== Configure `application.yml` - -Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. -To do so: - -. Go to `application.yml` and set the following configuration: -+ -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: <1> - google: <2> - client-id: google-client-id - client-secret: google-client-secret ----- -+ -.OAuth Client properties -==== -<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration], such as google. -==== - -. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. - - -[[webflux-oauth2-login-sample-start]] -=== Boot up the application - -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. -You are then redirected to the default _auto-generated_ login page, which displays a link for Google. - -Click on the Google link, and you are then redirected to Google for authentication. - -After authenticating with your Google account credentials, the next page presented to you is the Consent screen. -The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. -Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. - -At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. - -[[webflux-oauth2-login-openid-provider-configuration]] -== Using OpenID Provider Configuration - -For well known providers, Spring Security provides the necessary defaults for the OAuth Authorization Provider's configuration. -If you are working with your own Authorization Provider that supports https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration] or https://tools.ietf.org/html/rfc8414#section-3[Authorization Server Metadata], the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OpenID Provider Configuration Response]'s `issuer-uri` can be used to configure the application. - -[source,yml] ----- -spring: - security: - oauth2: - client: - provider: - keycloak: - issuer-uri: https://idp.example.com/auth/realms/demo - registration: - keycloak: - client-id: spring-security - client-secret: 6cea952f-10d0-4d00-ac79-cc865820dc2c ----- - -The `issuer-uri` instructs Spring Security to query in series the endpoints `https://idp.example.com/auth/realms/demo/.well-known/openid-configuration`, `https://idp.example.com/.well-known/openid-configuration/auth/realms/demo`, or `https://idp.example.com/.well-known/oauth-authorization-server/auth/realms/demo` to discover the configuration. - -[NOTE] -Spring Security will query the endpoints one at a time, stopping at the first that gives a 200 response. - -The `client-id` and `client-secret` are linked to the provider because `keycloak` is used for both the provider and the registration. - - -[[webflux-oauth2-login-explicit]] -== Explicit OAuth2 Login Configuration - -A minimal OAuth2 Login configuration is shown below: - -.Minimal OAuth2 Login -==== -.Java -[source,java,role="primary"] ----- -@Bean -ReactiveClientRegistrationRepository clientRegistrations() { - ClientRegistration clientRegistration = ClientRegistrations - .fromIssuerLocation("https://idp.example.com/auth/realms/demo") - .clientId("spring-security") - .clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c") - .build(); - return new InMemoryReactiveClientRegistrationRepository(clientRegistration); -} - -@Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - // ... - .oauth2Login(withDefaults()); - return http.build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun clientRegistrations(): ReactiveClientRegistrationRepository { - val clientRegistration: ClientRegistration = ClientRegistrations - .fromIssuerLocation("https://idp.example.com/auth/realms/demo") - .clientId("spring-security") - .clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c") - .build() - return InMemoryReactiveClientRegistrationRepository(clientRegistration) -} - -@Bean -fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { } - } -} ----- -==== - -Additional configuration options can be seen below: - -.Advanced OAuth2 Login -==== -.Java -[source,java,role="primary"] ----- -@Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - // ... - .oauth2Login(oauth2 -> oauth2 - .authenticationConverter(converter) - .authenticationManager(manager) - .authorizedClientRepository(authorizedClients) - .clientRegistrationRepository(clientRegistrations) - ); - return http.build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { - authenticationConverter = converter - authenticationManager = manager - authorizedClientRepository = authorizedClients - clientRegistrationRepository = clientRegistration - } - } -} ----- -==== - -You may register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the default configuration, as shown in the following example: - -.GrantedAuthoritiesMapper Bean -==== -.Java -[source,java,role="primary"] ----- -@Bean -public GrantedAuthoritiesMapper userAuthoritiesMapper() { - ... -} - -@Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - // ... - .oauth2Login(withDefaults()); - return http.build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun userAuthoritiesMapper(): GrantedAuthoritiesMapper { - // ... -} - -@Bean -fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { } - } -} ----- -==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc new file mode 100644 index 00000000000..8dfb8c9e935 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -0,0 +1,743 @@ +[[webflux-oauth2-login-advanced]] += Advanced Configuration + +The OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-3[Protocol Endpoints] as follows: + +The authorization process utilizes two authorization server endpoints (HTTP resources): + +* Authorization Endpoint: Used by the client to obtain authorization from the resource owner via user-agent redirection. +* Token Endpoint: Used by the client to exchange an authorization grant for an access token, typically with client authentication. + +As well as one client endpoint: + +* Redirection Endpoint: Used by the authorization server to return responses containing authorization credentials to the client via the resource owner user-agent. + +The OpenID Connect Core 1.0 specification defines the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] as follows: + +The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns claims about the authenticated end-user. +To obtain the requested claims about the end-user, the client makes a request to the UserInfo Endpoint by using an access token obtained through OpenID Connect Authentication. +These claims are normally represented by a JSON object that contains a collection of name-value pairs for the claims. + +`ServerHttpSecurity.oauth2Login()` provides a number of configuration options for customizing OAuth 2.0 Login. + +The following code shows the complete configuration options available for the `oauth2Login()` DSL: + +.OAuth2 Login Configuration Options +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .oauth2Login(oauth2 -> oauth2 + .authenticationConverter(this.authenticationConverter()) + .authenticationMatcher(this.authenticationMatcher()) + .authenticationManager(this.authenticationManager()) + .authenticationSuccessHandler(this.authenticationSuccessHandler()) + .authenticationFailureHandler(this.authenticationFailureHandler()) + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizedClientService(this.authorizedClientService()) + .authorizationRequestResolver(this.authorizationRequestResolver()) + .authorizationRequestRepository(this.authorizationRequestRepository()) + .securityContextRepository(this.securityContextRepository()) + ); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authenticationConverter = authenticationConverter() + authenticationMatcher = authenticationMatcher() + authenticationManager = authenticationManager() + authenticationSuccessHandler = authenticationSuccessHandler() + authenticationFailureHandler = authenticationFailureHandler() + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizedClientService = authorizedClientService() + authorizationRequestResolver = authorizationRequestResolver() + authorizationRequestRepository = authorizationRequestRepository() + securityContextRepository = securityContextRepository() + } + } + } +} +---- +==== + +The following sections go into more detail on each of the configuration options available: + +* <> +* <> +* <> +* <> +* <> + + +[[webflux-oauth2-login-advanced-login-page]] +== OAuth 2.0 Login Page + +By default, the OAuth 2.0 Login Page is auto-generated by the `LoginPageGeneratingWebFilter`. +The default login page shows each configured OAuth Client with its `ClientRegistration.clientName` as a link, which is capable of initiating the Authorization Request (or OAuth 2.0 Login). + +[NOTE] +In order for `LoginPageGeneratingWebFilter` to show links for configured OAuth Clients, the registered `ReactiveClientRegistrationRepository` needs to also implement `Iterable`. +See `InMemoryReactiveClientRegistrationRepository` for reference. + +The link's destination for each OAuth Client defaults to the following: + +`+"/oauth2/authorization/{registrationId}"+` + +The following line shows an example: + +[source,html] +---- +Google +---- + +To override the default login page, configure the `exceptionHandling().authenticationEntryPoint()` and (optionally) `oauth2Login().authorizationRequestResolver()`. + +The following listing shows an example: + +.OAuth2 Login Page Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2")) + ) + .oauth2Login(oauth2 -> oauth2 + .authorizationRequestResolver(this.authorizationRequestResolver()) + ); + + return http.build(); + } + + private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() { + ServerWebExchangeMatcher authorizationRequestMatcher = + new PathPatternParserServerWebExchangeMatcher( + "/login/oauth2/authorization/{registrationId}"); + + return new DefaultServerOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository(), authorizationRequestMatcher); + } + + ... +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + exceptionHandling { + authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2") + } + oauth2Login { + authorizationRequestResolver = authorizationRequestResolver() + } + } + } + + private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver { + val authorizationRequestMatcher: ServerWebExchangeMatcher = PathPatternParserServerWebExchangeMatcher( + "/login/oauth2/authorization/{registrationId}" + ) + + return DefaultServerOAuth2AuthorizationRequestResolver( + clientRegistrationRepository(), authorizationRequestMatcher + ) + } + + ... +} +---- +==== + +[IMPORTANT] +You need to provide a `@Controller` with a `@RequestMapping("/login/oauth2")` that is capable of rendering the custom login page. + +[TIP] +==== +As noted earlier, configuring `oauth2Login().authorizationRequestResolver()` is optional. +However, if you choose to customize it, ensure the link to each OAuth Client matches the pattern provided through the `ServerWebExchangeMatcher`. + +The following line shows an example: + +[source,html] +---- +Google +---- +==== + + +[[webflux-oauth2-login-advanced-redirection-endpoint]] +== Redirection Endpoint + +The Redirection Endpoint is used by the Authorization Server for returning the Authorization Response (which contains the authorization credentials) to the client via the Resource Owner user-agent. + +[TIP] +OAuth 2.0 Login leverages the Authorization Code Grant. +Therefore, the authorization credential is the authorization code. + +The default Authorization Response redirection endpoint is `/login/oauth2/code/{registrationId}`. + +If you would like to customize the Authorization Response redirection endpoint, configure it as shown in the following example: + +.Redirection Endpoint Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Login(oauth2 -> oauth2 + .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")) + ); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}") + } + } + } +} +---- +==== + +[IMPORTANT] +==== +You also need to ensure the `ClientRegistration.redirectUri` matches the custom Authorization Response redirection endpoint. + +The following listing shows an example: + +.Java +[source,java,role="primary",attrs="-attributes"] +---- +return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}") + .build(); +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}") + .build() +---- +==== + + +[[webflux-oauth2-login-advanced-userinfo-endpoint]] +== UserInfo Endpoint + +The UserInfo Endpoint includes a number of configuration options, as described in the following sub-sections: + +* <> +* <> +* <> + + +[[webflux-oauth2-login-advanced-map-authorities]] +=== Mapping User Authorities + +After the user successfully authenticates with the OAuth 2.0 Provider, the `OAuth2User.getAuthorities()` (or `OidcUser.getAuthorities()`) may be mapped to a new set of `GrantedAuthority` instances, which will be supplied to `OAuth2AuthenticationToken` when completing the authentication. + +[TIP] +`OAuth2AuthenticationToken.getAuthorities()` is used for authorizing requests, such as in `hasRole('USER')` or `hasRole('ADMIN')`. + +There are a couple of options to choose from when mapping user authorities: + +* <> +* <> + + +[[webflux-oauth2-login-advanced-map-authorities-grantedauthoritiesmapper]] +==== Using a GrantedAuthoritiesMapper + +Register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the configuration, as shown in the following example: + +.Granted Authorities Mapper Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public GrantedAuthoritiesMapper userAuthoritiesMapper() { + return (authorities) -> { + Set mappedAuthorities = new HashSet<>(); + + authorities.forEach(authority -> { + if (OidcUserAuthority.class.isInstance(authority)) { + OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority; + + OidcIdToken idToken = oidcUserAuthority.getIdToken(); + OidcUserInfo userInfo = oidcUserAuthority.getUserInfo(); + + // Map the claims found in idToken and/or userInfo + // to one or more GrantedAuthority's and add it to mappedAuthorities + + } else if (OAuth2UserAuthority.class.isInstance(authority)) { + OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority; + + Map userAttributes = oauth2UserAuthority.getAttributes(); + + // Map the attributes found in userAttributes + // to one or more GrantedAuthority's and add it to mappedAuthorities + + } + }); + + return mappedAuthorities; + }; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection -> + val mappedAuthorities = emptySet() + + authorities.forEach { authority -> + if (authority is OidcUserAuthority) { + val idToken = authority.idToken + val userInfo = authority.userInfo + // Map the claims found in idToken and/or userInfo + // to one or more GrantedAuthority's and add it to mappedAuthorities + } else if (authority is OAuth2UserAuthority) { + val userAttributes = authority.attributes + // Map the attributes found in userAttributes + // to one or more GrantedAuthority's and add it to mappedAuthorities + } + } + + mappedAuthorities + } +} +---- +==== + +[[webflux-oauth2-login-advanced-map-authorities-reactiveoauth2userservice]] +==== Delegation-based strategy with ReactiveOAuth2UserService + +This strategy is advanced compared to using a `GrantedAuthoritiesMapper`, however, it's also more flexible as it gives you access to the `OAuth2UserRequest` and `OAuth2User` (when using an OAuth 2.0 UserService) or `OidcUserRequest` and `OidcUser` (when using an OpenID Connect 1.0 UserService). + +The `OAuth2UserRequest` (and `OidcUserRequest`) provides you access to the associated `OAuth2AccessToken`, which is very useful in the cases where the _delegator_ needs to fetch authority information from a protected resource before it can map the custom authorities for the user. + +The following example shows how to implement and configure a delegation-based strategy using an OpenID Connect 1.0 UserService: + +.ReactiveOAuth2UserService Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService(); + + return (userRequest) -> { + // Delegate to the default implementation for loading a user + return delegate.loadUser(userRequest) + .flatMap((oidcUser) -> { + OAuth2AccessToken accessToken = userRequest.getAccessToken(); + Set mappedAuthorities = new HashSet<>(); + + // TODO + // 1) Fetch the authority information from the protected resource using accessToken + // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + + // 3) Create a copy of oidcUser but use the mappedAuthorities instead + oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); + + return Mono.just(oidcUser); + }); + }; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun oidcUserService(): ReactiveOAuth2UserService { + val delegate = OidcReactiveOAuth2UserService() + + return ReactiveOAuth2UserService { userRequest -> + // Delegate to the default implementation for loading a user + delegate.loadUser(userRequest) + .flatMap { oidcUser -> + val accessToken = userRequest.accessToken + val mappedAuthorities = mutableSetOf() + + // TODO + // 1) Fetch the authority information from the protected resource using accessToken + // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + // 3) Create a copy of oidcUser but use the mappedAuthorities instead + val mappedOidcUser = DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo) + + Mono.just(mappedOidcUser) + } + } + } +} +---- +==== + + +[[webflux-oauth2-login-advanced-oauth2-user-service]] +=== OAuth 2.0 UserService + +`DefaultReactiveOAuth2UserService` is an implementation of a `ReactiveOAuth2UserService` that supports standard OAuth 2.0 Provider's. + +[NOTE] +`ReactiveOAuth2UserService` obtains the user attributes of the end-user (the resource owner) from the UserInfo Endpoint (by using the access token granted to the client during the authorization flow) and returns an `AuthenticatedPrincipal` in the form of an `OAuth2User`. + +`DefaultReactiveOAuth2UserService` uses a `WebClient` when requesting the user attributes at the UserInfo Endpoint. + +If you need to customize the pre-processing of the UserInfo Request and/or the post-handling of the UserInfo Response, you will need to provide `DefaultReactiveOAuth2UserService.setWebClient()` with a custom configured `WebClient`. + +Whether you customize `DefaultReactiveOAuth2UserService` or provide your own implementation of `ReactiveOAuth2UserService`, you'll need to configure it as shown in the following example: + +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oauth2UserService() { + ... + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun oauth2UserService(): ReactiveOAuth2UserService { + // ... + } +} +---- +==== + + +[[webflux-oauth2-login-advanced-oidc-user-service]] +=== OpenID Connect 1.0 UserService + +`OidcReactiveOAuth2UserService` is an implementation of a `ReactiveOAuth2UserService` that supports OpenID Connect 1.0 Provider's. + +The `OidcReactiveOAuth2UserService` leverages the `DefaultReactiveOAuth2UserService` when requesting the user attributes at the UserInfo Endpoint. + +If you need to customize the pre-processing of the UserInfo Request and/or the post-handling of the UserInfo Response, you will need to provide `OidcReactiveOAuth2UserService.setOauth2UserService()` with a custom configured `ReactiveOAuth2UserService`. + +Whether you customize `OidcReactiveOAuth2UserService` or provide your own implementation of `ReactiveOAuth2UserService` for OpenID Connect 1.0 Provider's, you'll need to configure it as shown in the following example: + +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + ... + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun oidcUserService(): ReactiveOAuth2UserService { + // ... + } +} +---- +==== + + +[[webflux-oauth2-login-advanced-idtoken-verify]] +== ID Token Signature Verification + +OpenID Connect 1.0 Authentication introduces the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token], which is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when used by a Client. + +The ID Token is represented as a https://tools.ietf.org/html/rfc7519[JSON Web Token] (JWT) and MUST be signed using https://tools.ietf.org/html/rfc7515[JSON Web Signature] (JWS). + +The `ReactiveOidcIdTokenDecoderFactory` provides a `ReactiveJwtDecoder` used for `OidcIdToken` signature verification. The default algorithm is `RS256` but may be different when assigned during client registration. +For these cases, a resolver may be configured to return the expected JWS algorithm assigned for a specific client. + +The JWS algorithm resolver is a `Function` that accepts a `ClientRegistration` and returns the expected `JwsAlgorithm` for the client, eg. `SignatureAlgorithm.RS256` or `MacAlgorithm.HS256` + +The following code shows how to configure the `OidcIdTokenDecoderFactory` `@Bean` to default to `MacAlgorithm.HS256` for all `ClientRegistration`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveJwtDecoderFactory idTokenDecoderFactory() { + ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory(); + idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256); + return idTokenDecoderFactory; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory { + val idTokenDecoderFactory = ReactiveOidcIdTokenDecoderFactory() + idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 } + return idTokenDecoderFactory +} +---- +==== + +[NOTE] +For MAC based algorithms such as `HS256`, `HS384` or `HS512`, the `client-secret` corresponding to the `client-id` is used as the symmetric key for signature verification. + +[TIP] +If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return. + + +[[webflux-oauth2-login-advanced-oidc-logout]] +== OpenID Connect 1.0 Logout + +OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. +One of the strategies available is https://openid.net/specs/openid-connect-session-1_0.html#RPLogout[RP-Initiated Logout]. + +If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client may obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata]. +This can be achieved by configuring the `ClientRegistration` with the `issuer-uri`, as in the following example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + ... + provider: + okta: + issuer-uri: https://dev-1234.oktapreview.com +---- + +...and the `OidcClientInitiatedServerLogoutSuccessHandler`, which implements RP-Initiated Logout, may be configured as follows: + +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()) + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + ); + + return http.build(); + } + + private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedServerLogoutSuccessHandler(this.clientRegistrationRepository); + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); + + return oidcLogoutSuccessHandler; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + logout { + logoutSuccessHandler = oidcLogoutSuccessHandler() + } + } + } + + private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { + val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository) + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") + return oidcLogoutSuccessHandler + } +} +---- +==== + +NOTE: `OidcClientInitiatedServerLogoutSuccessHandler` supports the `{baseUrl}` placeholder. +If used, the application's base URL, like `https://app.example.org`, will replace it at request time. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc new file mode 100644 index 00000000000..037fcff5f11 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc @@ -0,0 +1,539 @@ += Core Configuration + +[[webflux-oauth2-login-sample]] +== Spring Boot 2.x Sample + +Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. + +This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: + +* <> +* <> +* <> +* <> + + +[[webflux-oauth2-login-sample-setup]] +=== Initial setup + +To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. + +NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. + +Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". + +After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. + + +[[webflux-oauth2-login-sample-redirect]] +=== Setting the redirect URI + +The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. + +In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. + +TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. +The *_registrationId_* is a unique identifier for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration]. +For our example, the `registrationId` is `google`. + +IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. +Also, see the supported xref:reactive/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. + + +[[webflux-oauth2-login-sample-config]] +=== Configure `application.yml` + +Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. +To do so: + +. Go to `application.yml` and set the following configuration: ++ +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: <1> + google: <2> + client-id: google-client-id + client-secret: google-client-secret +---- ++ +.OAuth Client properties +==== +<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. +<2> Following the base property prefix is the ID for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. + + +[[webflux-oauth2-login-sample-start]] +=== Boot up the application + +Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. +You are then redirected to the default _auto-generated_ login page, which displays a link for Google. + +Click on the Google link, and you are then redirected to Google for authentication. + +After authenticating with your Google account credentials, the next page presented to you is the Consent screen. +The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. +Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. + +At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. + + +[[oauth2login-boot-property-mappings]] +== Spring Boot 2.x Property Mappings + +The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration] properties. + +|=== +|Spring Boot 2.x |ClientRegistration + +|`spring.security.oauth2.client.registration._[registrationId]_` +|`registrationId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-id` +|`clientId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` +|`clientSecret` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` +|`clientAuthenticationMethod` + +|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` +|`authorizationGrantType` + +|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` +|`redirectUri` + +|`spring.security.oauth2.client.registration._[registrationId]_.scope` +|`scopes` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-name` +|`clientName` + +|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` +|`providerDetails.authorizationUri` + +|`spring.security.oauth2.client.provider._[providerId]_.token-uri` +|`providerDetails.tokenUri` + +|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` +|`providerDetails.jwkSetUri` + +|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` +|`providerDetails.issuerUri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` +|`providerDetails.userInfoEndpoint.uri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` +|`providerDetails.userInfoEndpoint.authenticationMethod` + +|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` +|`providerDetails.userInfoEndpoint.userNameAttributeName` +|=== + +[TIP] +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. + + +[[webflux-oauth2-login-common-oauth2-provider]] +== CommonOAuth2Provider + +`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. + +For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. +Therefore, it makes sense to provide default values in order to reduce the required configuration. + +As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google: + client-id: google-client-id + client-secret: google-client-secret +---- + +[TIP] +The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. + +For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google-login: <1> + provider: google <2> + client-id: google-client-id + client-secret: google-client-secret +---- +<1> The `registrationId` is set to `google-login`. +<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. + + +[[webflux-oauth2-login-custom-provider-properties]] +== Configuring Custom Provider Properties + +There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). + +For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. + +For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + provider: + okta: <1> + authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize + token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token + user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo + user-name-attribute: sub + jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys +---- + +<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. + + +[[webflux-oauth2-login-override-boot-autoconfig]] +== Overriding Spring Boot 2.x Auto-configuration + +The Spring Boot 2.x auto-configuration class for OAuth Client support is `ReactiveOAuth2ClientAutoConfiguration`. + +It performs the following tasks: + +* Registers a `ReactiveClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. +* Registers a `SecurityWebFilterChain` `@Bean` and enables OAuth 2.0 Login through `serverHttpSecurity.oauth2Login()`. + +If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: + +* <> +* <> +* <> + + +[[webflux-oauth2-login-register-reactiveclientregistrationrepository-bean]] +=== Register a ReactiveClientRegistrationRepository @Bean + +The following example shows how to register a `ReactiveClientRegistrationRepository` `@Bean`: + +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@Configuration +public class OAuth2LoginConfig { + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@Configuration +class OAuth2LoginConfig { + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[webflux-oauth2-login-register-securitywebfilterchain-bean]] +=== Register a SecurityWebFilterChain @Bean + +The following example shows how to register a `SecurityWebFilterChain` `@Bean` with `@EnableWebFluxSecurity` and enable OAuth 2.0 login through `serverHttpSecurity.oauth2Login()`: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + } +} +---- +==== + + +[[webflux-oauth2-login-completely-override-autoconfiguration]] +=== Completely Override the Auto-configuration + +The following example shows how to completely override the auto-configuration by registering a `ReactiveClientRegistrationRepository` `@Bean` and a `SecurityWebFilterChain` `@Bean`. + +.Overriding the auto-configuration +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[webflux-oauth2-login-javaconfig-wo-boot]] +== Java Configuration without Spring Boot 2.x + +If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + @Bean + public ReactiveOAuth2AuthorizedClientService authorizedClientService( + ReactiveClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public ServerOAuth2AuthorizedClientRepository authorizedClientRepository( + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService); + } + + private ClientRegistration googleClientRegistration() { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + @Bean + fun authorizedClientService( + clientRegistrationRepository: ReactiveClientRegistrationRepository + ): ReactiveOAuth2AuthorizedClientService { + return InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository) + } + + @Bean + fun authorizedClientRepository( + authorizedClientService: ReactiveOAuth2AuthorizedClientService + ): ServerOAuth2AuthorizedClientRepository { + return AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService) + } + + private fun googleClientRegistration(): ClientRegistration { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build() + } +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc new file mode 100644 index 00000000000..878398ef90d --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc @@ -0,0 +1,8 @@ +[[webflux-oauth2-login]] += OAuth 2.0 Login +:page-section-summary-toc: 1 + +The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). +OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". + +NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc index 9138926b797..0b713f306d1 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc @@ -1,4 +1,4 @@ -= OAuth 2.0 Resource Server Multitenancy += OAuth 2.0 Resource Server Multi-tenancy [[webflux-oauth2resourceserver-multitenancy]] == Multi-tenancy diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc index 70b6bd5133c..e6078612069 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc @@ -23,13 +23,14 @@ To specify where the introspection endpoint is, simply do: [source,yaml] ---- -security: - oauth2: - resourceserver: - opaque-token: - introspection-uri: https://idp.example.com/introspect - client-id: client - client-secret: secret +spring: + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://idp.example.com/introspect + client-id: client + client-secret: secret ---- Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint. diff --git a/docs/modules/ROOT/pages/reactive/test/method.adoc b/docs/modules/ROOT/pages/reactive/test/method.adoc index 2c51dd24baa..ddbaa6b09bd 100644 --- a/docs/modules/ROOT/pages/reactive/test/method.adoc +++ b/docs/modules/ROOT/pages/reactive/test/method.adoc @@ -8,7 +8,7 @@ Here is a minimal sample of what we can do: .Java [source,java,role="primary"] ---- -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = HelloWebfluxMethodApplication.class) public class HelloWorldMessageServiceTests { @Autowired @@ -42,7 +42,7 @@ public class HelloWorldMessageServiceTests { .Kotlin [source,kotlin,role="secondary"] ---- -@RunWith(SpringRunner::class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = [HelloWebfluxMethodApplication::class]) class HelloWorldMessageServiceTests { @Autowired diff --git a/docs/modules/ROOT/pages/reactive/test/web/setup.adoc b/docs/modules/ROOT/pages/reactive/test/web/setup.adoc index d84d0c50426..ca63529ea45 100644 --- a/docs/modules/ROOT/pages/reactive/test/web/setup.adoc +++ b/docs/modules/ROOT/pages/reactive/test/web/setup.adoc @@ -4,7 +4,7 @@ The basic setup looks like this: [source,java] ---- -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = HelloWebfluxMethodApplication.class) public class HelloWebfluxMethodApplicationTests { @Autowired @@ -12,7 +12,7 @@ public class HelloWebfluxMethodApplicationTests { WebTestClient rest; - @Before + @BeforeEach public void setup() { this.rest = WebTestClient .bindToApplicationContext(this.context) diff --git a/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc b/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc index f6cf413b681..17cd707fe18 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc @@ -367,7 +367,7 @@ END; [[dbschema-oauth2-client]] == OAuth 2.0 Client Schema -The JDBC implementation of xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-authorized-repo-service[ OAuth2AuthorizedClientService] (`JdbcOAuth2AuthorizedClientService`) requires a table for persisting `OAuth2AuthorizedClient`(s). +The JDBC implementation of xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-repo-service[ OAuth2AuthorizedClientService] (`JdbcOAuth2AuthorizedClientService`) requires a table for persisting `OAuth2AuthorizedClient`(s). You will need to adjust this schema to match the database dialect you are using. [source,ddl] diff --git a/docs/modules/ROOT/pages/servlet/appendix/index.adoc b/docs/modules/ROOT/pages/servlet/appendix/index.adoc index 3fecd174b47..9c84348fea4 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/index.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/index.adoc @@ -4,5 +4,5 @@ This is an appendix for Servlet based Spring Security. It has the following sections: * xref:servlet/appendix/database-schema.adoc[Database Schemas] -* xref:servlet/appendix/namespace.adoc[XML Namespace] +* xref:servlet/appendix/namespace/index.adoc[XML Namespace] * xref:servlet/appendix/faq.adoc[FAQ] diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc new file mode 100644 index 00000000000..5452a2b7994 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc @@ -0,0 +1,292 @@ +[[nsa-authentication]] += Authentication Services +Before Spring Security 3.0, an `AuthenticationManager` was automatically registered internally. +Now you must register one explicitly using the `` element. +This creates an instance of Spring Security's `ProviderManager` class, which needs to be configured with a list of one or more `AuthenticationProvider` instances. +These can either be created using syntax elements provided by the namespace, or they can be standard bean definitions, marked for addition to the list using the `authentication-provider` element. + + +[[nsa-authentication-manager]] +== +Every Spring Security application which uses the namespace must have include this element somewhere. +It is responsible for registering the `AuthenticationManager` which provides authentication services to the application. +All elements which create `AuthenticationProvider` instances should be children of this element. + + +[[nsa-authentication-manager-attributes]] +=== Attributes + + +[[nsa-authentication-manager-alias]] +* **alias** +This attribute allows you to define an alias name for the internal instance for use in your own configuration. + + +[[nsa-authentication-manager-erase-credentials]] +* **erase-credentials** +If set to true, the AuthenticationManager will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. +Literally it maps to the `eraseCredentialsAfterAuthentication` property of the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. + + +[[nsa-authentication-manager-id]] +* **id** +This attribute allows you to define an id for the internal instance for use in your own configuration. +It is the same as the alias element, but provides a more consistent experience with elements that use the id attribute. + + +[[nsa-authentication-manager-children]] +=== Child Elements of + + +* <> +* xref:servlet/appendix/namespace/ldap.adoc#nsa-ldap-authentication-provider[ldap-authentication-provider] + + + +[[nsa-authentication-provider]] +== +Unless used with a `ref` attribute, this element is shorthand for configuring a `DaoAuthenticationProvider`. +`DaoAuthenticationProvider` loads user information from a `UserDetailsService` and compares the username/password combination with the values supplied at login. +The `UserDetailsService` instance can be defined either by using an available namespace element (`jdbc-user-service` or by using the `user-service-ref` attribute to point to a bean defined elsewhere in the application context). + + + +[[nsa-authentication-provider-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-authentication-provider-attributes]] +=== Attributes + + +[[nsa-authentication-provider-ref]] +* **ref** +Defines a reference to a Spring bean that implements `AuthenticationProvider`. + +If you have written your own `AuthenticationProvider` implementation (or want to configure one of Spring Security's own implementations as a traditional bean for some reason, then you can use the following syntax to add it to the internal list of `ProviderManager`: + +[source,xml] +---- + + + + + + +---- + + + + +[[nsa-authentication-provider-user-service-ref]] +* **user-service-ref** +A reference to a bean that implements UserDetailsService that may be created using the standard bean element or the custom user-service element. + + +[[nsa-authentication-provider-children]] +=== Child Elements of + + +* <> +* xref:servlet/appendix/namespace/ldap.adoc#nsa-ldap-user-service[ldap-user-service] +* <> +* <> + + + +[[nsa-jdbc-user-service]] +== +Causes creation of a JDBC-based UserDetailsService. + + +[[nsa-jdbc-user-service-attributes]] +=== Attributes + + +[[nsa-jdbc-user-service-authorities-by-username-query]] +* **authorities-by-username-query** +An SQL statement to query for a user's granted authorities given a username. + +The default is + +[source] +---- +select username, authority from authorities where username = ? +---- + + + + +[[nsa-jdbc-user-service-cache-ref]] +* **cache-ref** +Defines a reference to a cache for use with a UserDetailsService. + + +[[nsa-jdbc-user-service-data-source-ref]] +* **data-source-ref** +The bean ID of the DataSource which provides the required tables. + + +[[nsa-jdbc-user-service-group-authorities-by-username-query]] +* **group-authorities-by-username-query** +An SQL statement to query user's group authorities given a username. +The default is + ++ + +[source] +---- +select +g.id, g.group_name, ga.authority +from +groups g, group_members gm, group_authorities ga +where +gm.username = ? and g.id = ga.group_id and g.id = gm.group_id +---- + + + + +[[nsa-jdbc-user-service-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-jdbc-user-service-role-prefix]] +* **role-prefix** +A non-empty string prefix that will be added to role strings loaded from persistent storage (default is "ROLE_"). +Use the value "none" for no prefix in cases where the default is non-empty. + + +[[nsa-jdbc-user-service-users-by-username-query]] +* **users-by-username-query** +An SQL statement to query a username, password, and enabled status given a username. +The default is + ++ + +[source] +---- +select username, password, enabled from users where username = ? +---- + + + + +[[nsa-password-encoder]] +== +Authentication providers can optionally be configured to use a password encoder as described in the xref:features/authentication/password-storage.adoc#authentication-password-storage[Password Storage]. +This will result in the bean being injected with the appropriate `PasswordEncoder` instance. + + +[[nsa-password-encoder-parents]] +=== Parent Elements of + + +* <> +* xref:servlet/appendix/namespace/authentication-manager.adoc#nsa-password-compare[password-compare] + + + +[[nsa-password-encoder-attributes]] +=== Attributes + + +[[nsa-password-encoder-hash]] +* **hash** +Defines the hashing algorithm used on user passwords. +We recommend strongly against using MD4, as it is a very weak hashing algorithm. + + +[[nsa-password-encoder-ref]] +* **ref** +Defines a reference to a Spring bean that implements `PasswordEncoder`. + + +[[nsa-user-service]] +== +Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. +Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + + +[[nsa-user-service-attributes]] +=== Attributes + + +[[nsa-user-service-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-user-service-properties]] +* **properties** +The location of a Properties file where each line is in the format of + ++ + +[source] +---- +username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] +---- + + + + +[[nsa-user-service-children]] +=== Child Elements of + + +* <> + + + +[[nsa-user]] +== +Represents a user in the application. + + +[[nsa-user-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-user-attributes]] +=== Attributes + + +[[nsa-user-authorities]] +* **authorities** +One of more authorities granted to the user. +Separate authorities with a comma (but no space). +For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + +[[nsa-user-disabled]] +* **disabled** +Can be set to "true" to mark an account as disabled and unusable. + + +[[nsa-user-locked]] +* **locked** +Can be set to "true" to mark an account as locked and unusable. + + +[[nsa-user-name]] +* **name** +The username assigned to the user. + + +[[nsa-user-password]] +* **password** +The password assigned to the user. +This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). +This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. +If omitted, the namespace will generate a random value, preventing its accidental use for authentication. +Cannot be empty. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc similarity index 61% rename from docs/modules/ROOT/pages/servlet/appendix/namespace.adoc rename to docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index a6518b4ed13..1f26ad0f45f 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -1,22 +1,14 @@ -[[appendix-namespace]] -= The Security Namespace -This appendix provides a reference to the elements available in the security namespace and information on the underlying beans they create (a knowledge of the individual classes and how they work together is assumed - you can find more information in the project Javadoc and elsewhere in this document). -If you haven't used the namespace before, please read the xref:servlet/configuration/xml-namespace.adoc#ns-config[introductory chapter] on namespace configuration, as this is intended as a supplement to the information there. -Using a good quality XML editor while editing a configuration based on the schema is recommended as this will provide contextual information on which elements and attributes are available as well as comments explaining their purpose. -The namespace is written in https://relaxng.org/[RELAX NG] Compact format and later converted into an XSD schema. -If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc[schema file] directly. - [[nsa-web]] -== Web Application Security += Web Application Security [[nsa-debug]] -=== +== Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. [[nsa-http]] -=== +== If you use an `` element within your application, a `FilterChainProxy` bean named "springSecurityFilterChain" is created and the configuration within the element is used to build a filter chain within `FilterChainProxy`. As of Spring Security 3.1, additional `http` elements can be used to add extra filter chains footnote:[See the pass:specialcharacters,macros[xref:servlet/configuration/xml-namespace.adoc#ns-web-xml[introductory chapter]] for how to set up the mapping from your `web.xml` ]. @@ -34,9 +26,16 @@ These are fixed and cannot be replaced with alternatives. [[nsa-http-attributes]] -==== Attributes +=== Attributes The attributes on the `` element control some of the properties on the core filters. +[[nsa-http-use-authorization-manager]] +* **use-authorization-manager** +Use AuthorizationManager API instead of SecurityMetadataSource + +[[nsa-http-authorization-manager-ref]] +* **access-decision-manager-ref** +Use this AuthorizationManager instead of deriving one from elements [[nsa-http-access-decision-manager-ref]] * **access-decision-manager-ref** @@ -133,6 +132,12 @@ A request pattern can be mapped to an empty filter chain, by setting this attrib No security will be applied and none of Spring Security's features will be available. +[[nsa-http-security-context-explicit-save]] +* **security-context-explicit-save** +If true, use `SecurityContextHolderFilter` instead of `SecurityContextPersistenceFilter`. +Requires explicit save + + [[nsa-http-security-context-repository-ref]] * **security-context-repository-ref** Allows injection of a custom `SecurityContextRepository` into the `SecurityContextPersistenceFilter`. @@ -151,7 +156,7 @@ The default value is true. [[nsa-http-children]] -==== Child Elements of +=== Child Elements of * <> * <> * <> @@ -172,23 +177,25 @@ The default value is true. * <> * <> * <> +* <> +* <> * <> * <> [[nsa-access-denied-handler]] -=== -This element allows you to set the `errorPage` property for the default `AccessDeniedHandler` used by the `ExceptionTranslationFilter`, using the <> attribute, or to supply your own implementation using the<> attribute. +== +This element allows you to set the `errorPage` property for the default `AccessDeniedHandler` used by the `ExceptionTranslationFilter`, using the <> attribute, or to supply your own implementation using the <> attribute. This is discussed in more detail in the section on the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[ExceptionTranslationFilter]. [[nsa-access-denied-handler-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-access-denied-handler-attributes]] -==== Attributes +=== Attributes [[nsa-access-denied-handler-error-page]] @@ -202,12 +209,12 @@ Defines a reference to a Spring bean of type `AccessDeniedHandler`. [[nsa-cors]] -=== +== This element allows for configuring a `CorsFilter`. If no `CorsFilter` or `CorsConfigurationSource` is specified and Spring MVC is on the classpath, a `HandlerMappingIntrospector` is used as the `CorsConfigurationSource`. [[nsa-cors-attributes]] -==== Attributes +=== Attributes The attributes on the `` element control the headers element. [[nsa-cors-ref]] @@ -219,12 +226,12 @@ Optional attribute that specifies the bean name of a `CorsFilter`. Optional attribute that specifies the bean name of a `CorsConfigurationSource` to be injected into a `CorsFilter` created by the XML namespace. [[nsa-cors-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-headers]] -=== +== This element allows for configuring additional (security) headers to be send with the response. It enables easy configuration for several headers and also allows for setting custom headers through the <> element. Additional information, can be found in the xref:features/exploits/headers.adoc#headers[Security Headers] section of the reference. @@ -246,9 +253,12 @@ This allows HTTPS websites to resist impersonation by attackers using mis-issued https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). ** `Referrer-Policy` - Can be set using the <> element, https://www.w3.org/TR/referrer-policy/[Referrer-Policy] is a mechanism that web applications can leverage to manage the referrer field, which contains the last page the user was on. ** `Feature-Policy` - Can be set using the <> element, https://wicg.github.io/feature-policy/[Feature-Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. +** `Cross-Origin-Opener-Policy` - Can be set using the <> element, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy[Cross-Origin-Opener-Policy] is a mechanism that allows you to ensure a top-level document does not share a browsing context group with cross-origin documents. +** `Cross-Origin-Embedder-Policy` - Can be set using the <> element, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy[Cross-Origin-Embedder-Policy] is a mechanism that prevents a document from loading any cross-origin resources that don't explicitly grant the document permission. +** `Cross-Origin-Resource-Policy` - Can be set using the <> element, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy[Cross-Origin-Resource-Policy] is a mechanism that conveys a desire that the browser blocks no-cors cross-origin/cross-site requests to the given resource. [[nsa-headers-attributes]] -==== Attributes +=== Attributes The attributes on the `` element control the headers element. @@ -264,19 +274,22 @@ The default is false (the headers are enabled). [[nsa-headers-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-headers-children]] -==== Child Elements of +=== Child Elements of * <> * <> * <> +* <> +* <> +* <> * <> * <> * <> @@ -289,12 +302,12 @@ The default is false (the headers are enabled). [[nsa-cache-control]] -=== +== Adds `Cache-Control`, `Pragma`, and `Expires` headers to ensure that the browser does not cache your secured pages. [[nsa-cache-control-attributes]] -==== Attributes +=== Attributes [[nsa-cache-control-disabled]] * **disabled** @@ -303,7 +316,7 @@ Default false. [[nsa-cache-control-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -311,13 +324,13 @@ Default false. [[nsa-hsts]] -=== +== When enabled adds the https://tools.ietf.org/html/rfc6797[Strict-Transport-Security] header to the response for any secure request. This allows the server to instruct browsers to automatically use HTTPS for future requests. [[nsa-hsts-attributes]] -==== Attributes +=== Attributes [[nsa-hsts-disabled]] * **disabled** @@ -347,20 +360,20 @@ Specifies if preload should be included. Default false. [[nsa-hsts-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-hpkp]] -=== +== When enabled adds the https://tools.ietf.org/html/rfc7469[Public Key Pinning Extension for HTTP] header to the response for any secure request. This allows HTTPS websites to resist impersonation by attackers using mis-issued or otherwise fraudulent certificates. [[nsa-hpkp-attributes]] -==== Attributes +=== Attributes [[nsa-hpkp-disabled]] * **disabled** @@ -391,28 +404,28 @@ Specifies the URI to which the browser should report pin validation failures. [[nsa-hpkp-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-pins]] -=== +== The list of pins [[nsa-pins-children]] -==== Child Elements of +=== Child Elements of * <> [[nsa-pin]] -=== +== A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute [[nsa-pin-attributes]] -==== Attributes +=== Attributes [[nsa-pin-algorithm]] * **algorithm** @@ -421,19 +434,19 @@ Default is SHA256. [[nsa-pin-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-content-security-policy]] -=== +== When enabled adds the https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] header to the response. CSP is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). [[nsa-content-security-policy-attributes]] -==== Attributes +=== Attributes [[nsa-content-security-policy-policy-directives]] * **policy-directives** @@ -445,18 +458,18 @@ Set to true, to enable the Content-Security-Policy-Report-Only header for report Defaults to false. [[nsa-content-security-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-referrer-policy]] -=== +== When enabled adds the https://www.w3.org/TR/referrer-policy/[Referrer Policy] header to the response. [[nsa-referrer-policy-attributes]] -==== Attributes +=== Attributes [[nsa-referrer-policy-policy]] * **policy** @@ -464,37 +477,37 @@ The policy for the Referrer-Policy header. Default "no-referrer". [[nsa-referrer-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-feature-policy]] -=== +== When enabled adds the https://wicg.github.io/feature-policy/[Feature Policy] header to the response. [[nsa-feature-policy-attributes]] -==== Attributes +=== Attributes [[nsa-feature-policy-policy-directives]] * **policy-directives** The security policy directive(s) for the Feature-Policy header. [[nsa-feature-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-frame-options]] -=== +== When enabled adds the https://tools.ietf.org/html/draft-ietf-websec-x-frame-options[X-Frame-Options header] to the response, this allows newer browsers to do some security checks and prevent https://en.wikipedia.org/wiki/Clickjacking[clickjacking] attacks. [[nsa-frame-options-attributes]] -==== Attributes +=== Attributes [[nsa-frame-options-disabled]] * **disabled** @@ -515,34 +528,34 @@ On the other hand, if you specify SAMEORIGIN, you can still use the page in a fr [[nsa-frame-options-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-permissions-policy]] -=== +== Adds the https://w3c.github.io/webappsec-permissions-policy/[Permissions-Policy header] to the response. [[nsa-permissions-policy-attributes]] -==== Attributes +=== Attributes [[nsa-permissions-policy-policy]] * **policy** The policy value to write for the `Permissions-Policy` header [[nsa-permissions-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-xss-protection]] -=== +== Adds the https://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx[X-XSS-Protection header] to the response to assist in protecting against https://en.wikipedia.org/wiki/Cross-site_scripting#Non-Persistent[reflected / Type-1 Cross-Site Scripting (XSS)] attacks. This is in no-way a full protection to XSS attacks! [[nsa-xss-protection-attributes]] -==== Attributes +=== Attributes [[nsa-xss-protection-disabled]] @@ -564,20 +577,20 @@ Note that there are sometimes ways of bypassing this mode which can often times [[nsa-xss-protection-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-content-type-options]] -=== +== Add the X-Content-Type-Options header with the value of nosniff to the response. This https://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx[disables MIME-sniffing] for IE8+ and Chrome extensions. [[nsa-content-type-options-attributes]] -==== Attributes +=== Attributes [[nsa-content-type-options-disabled]] * **disabled** @@ -585,7 +598,67 @@ Specifies if Content Type Options should be disabled. Default false. [[nsa-content-type-options-parents]] -==== Parent Elements of +=== Parent Elements of + + +* <> + + + +[[nsa-cross-origin-embedder-policy]] +==== +When enabled adds the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy[Cross-Origin-Embedder-Policy] header to the response. + + +[[nsa-cross-origin-embedder-policy-attributes]] +===== Attributes + +[[nsa-cross-origin-embedder-policy-policy]] +* **policy** +The policy for the `Cross-Origin-Embedder-Policy` header. + +[[nsa-cross-origin-embedder-policy-parents]] +===== Parent Elements of + + +* <> + + + +[[nsa-cross-origin-opener-policy]] +==== +When enabled adds the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy[Cross-Origin-Opener-Policy] header to the response. + + +[[nsa-cross-origin-opener-policy-attributes]] +===== Attributes + +[[nsa-cross-origin-opener-policy-policy]] +* **policy** +The policy for the `Cross-Origin-Opener-Policy` header. + +[[nsa-cross-origin-opener-policy-parents]] +===== Parent Elements of + + +* <> + + + +[[nsa-cross-origin-resource-policy]] +==== +When enabled adds the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy[Cross-Origin-Resource-Policy] header to the response. + + +[[nsa-cross-origin-resource-policy-attributes]] +===== Attributes + +[[nsa-cross-origin-resource-policy-policy]] +* **policy** +The policy for the `Cross-Origin-Resource-Policy` header. + +[[nsa-cross-origin-resource-policy-parents]] +===== Parent Elements of * <> @@ -593,12 +666,12 @@ Default false. [[nsa-header]] -===

      +==
      Add additional headers to the response, both the name and value need to be specified. [[nsa-header-attributes]] -==== Attributes +=== Attributes [[nsa-header-name]] @@ -617,7 +690,7 @@ Reference to a custom implementation of the `HeaderWriter` interface. [[nsa-header-parents]] -==== Parent Elements of
      +=== Parent Elements of
      * <> @@ -625,13 +698,13 @@ Reference to a custom implementation of the `HeaderWriter` interface. [[nsa-anonymous]] -=== +== Adds an `AnonymousAuthenticationFilter` to the stack and an `AnonymousAuthenticationProvider`. Required if you are using the `IS_AUTHENTICATED_ANONYMOUSLY` attribute. [[nsa-anonymous-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -639,7 +712,7 @@ Required if you are using the `IS_AUTHENTICATED_ANONYMOUSLY` attribute. [[nsa-anonymous-attributes]] -==== Attributes +=== Attributes [[nsa-anonymous-enabled]] @@ -671,14 +744,14 @@ if unset, defaults to `anonymousUser`. [[nsa-csrf]] -=== +== This element will add https://en.wikipedia.org/wiki/Cross-site_request_forgery[Cross Site Request Forger (CSRF)] protection to the application. It also updates the default RequestCache to only replay "GET" requests upon successful authentication. Additional information can be found in the xref:features/exploits/csrf.adoc#csrf[Cross Site Request Forgery (CSRF)] section of the reference. [[nsa-csrf-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -686,7 +759,7 @@ Additional information can be found in the xref:features/exploits/csrf.adoc#csrf [[nsa-csrf-attributes]] -==== Attributes +=== Attributes [[nsa-csrf-disabled]] * **disabled** @@ -707,14 +780,14 @@ Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS". [[nsa-custom-filter]] -=== +== This element is used to add a filter to the filter chain. It doesn't create any additional beans but is used to select a bean of type `javax.servlet.Filter` which is already defined in the application context and add that at a particular position in the filter chain maintained by Spring Security. Full details can be found in the xref:servlet/configuration/xml-namespace.adoc#ns-custom-filters[ namespace chapter]. [[nsa-custom-filter-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -722,7 +795,7 @@ Full details can be found in the xref:servlet/configuration/xml-namespace.adoc#n [[nsa-custom-filter-attributes]] -==== Attributes +=== Attributes [[nsa-custom-filter-after]] @@ -749,24 +822,24 @@ Defines a reference to a Spring bean that implements `Filter`. [[nsa-expression-handler]] -=== +== Defines the `SecurityExpressionHandler` instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. [[nsa-expression-handler-parents]] -==== Parent Elements of +=== Parent Elements of -* <> +* xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[global-method-security] * <> -* <> -* <> +* xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[method-security] +* xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-message-broker[websocket-message-broker] [[nsa-expression-handler-attributes]] -==== Attributes +=== Attributes [[nsa-expression-handler-ref]] @@ -775,7 +848,7 @@ Defines a reference to a Spring bean that implements `SecurityExpressionHandler` [[nsa-form-login]] -=== +== Used to add an `UsernamePasswordAuthenticationFilter` to the filter stack and an `LoginUrlAuthenticationEntryPoint` to the application context to provide authentication on demand. This will always take precedence over other namespace-created entry points. If no attributes are supplied, a login page will be generated automatically at the URL "/login" footnote:[ @@ -785,7 +858,7 @@ The class `DefaultLoginPageGeneratingFilter` is responsible for rendering the lo [[nsa-form-login-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -793,7 +866,7 @@ The class `DefaultLoginPageGeneratingFilter` is responsible for rendering the lo [[nsa-form-login-attributes]] -==== Attributes +=== Attributes [[nsa-form-login-always-use-default-target]] @@ -870,17 +943,17 @@ Maps a `ForwardAuthenticationFailureHandler` to `authenticationFailureHandler` p [[nsa-oauth2-login]] -=== -The xref:servlet/oauth2/oauth2-login.adoc#oauth2login[OAuth 2.0 Login] feature configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. +== +The xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] feature configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. [[nsa-oauth2-login-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-oauth2-login-attributes]] -==== Attributes +=== Attributes [[nsa-oauth2-login-client-registration-repository-ref]] @@ -954,17 +1027,17 @@ Reference to the `JwtDecoderFactory` used by `OidcAuthorizationCodeAuthenticatio [[nsa-oauth2-client]] -=== -Configures xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Client] support. +== +Configures xref:servlet/oauth2/client/index.adoc#oauth2client[OAuth 2.0 Client] support. [[nsa-oauth2-client-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-oauth2-client-attributes]] -==== Attributes +=== Attributes [[nsa-oauth2-client-client-registration-repository-ref]] @@ -983,24 +1056,24 @@ Reference to the `OAuth2AuthorizedClientService`. [[nsa-oauth2-client-children]] -==== Child Elements of +=== Child Elements of * <> [[nsa-authorization-code-grant]] -=== -Configures xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-grant-support[OAuth 2.0 Authorization Code Grant]. +== +Configures xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-grant-support[OAuth 2.0 Authorization Code Grant]. [[nsa-authorization-code-grant-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-authorization-code-grant-attributes]] -==== Attributes +=== Attributes [[nsa-authorization-code-grant-authorization-request-repository-ref]] @@ -1019,30 +1092,30 @@ Reference to the `OAuth2AccessTokenResponseClient`. [[nsa-client-registrations]] -=== -A container element for client(s) registered (xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]) with an OAuth 2.0 or OpenID Connect 1.0 Provider. +== +A container element for client(s) registered (xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration]) with an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-client-registrations-children]] -==== Child Elements of +=== Child Elements of * <> * <> [[nsa-client-registration]] -=== +== Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-client-registration-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-client-registration-attributes]] -==== Attributes +=== Attributes [[nsa-client-registration-registration-id]] @@ -1093,18 +1166,18 @@ A reference to the associated provider. May reference a `` element or [[nsa-provider]] -=== +== The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-provider-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-provider-attributes]] -==== Attributes +=== Attributes [[nsa-provider-provider-id]] @@ -1148,23 +1221,23 @@ The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (J The URI used to initially configure a `ClientRegistration` using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. [[nsa-oauth2-resource-server]] -=== +== Adds a `BearerTokenAuthenticationFilter`, `BearerTokenAuthenticationEntryPoint`, and `BearerTokenAccessDeniedHandler` to the configuration. In addition, either `` or `` must be specified. [[nsa-oauth2-resource-server-parents]] -==== Parents Elements of +=== Parents Elements of * <> [[nsa-oauth2-resource-server-children]] -==== Child Elements of +=== Child Elements of * <> * <> [[nsa-oauth2-resource-server-attributes]] -==== Attributes +=== Attributes [[nsa-oauth2-resource-server-authentication-manager-resolver-ref]] * **authentication-manager-resolver-ref** @@ -1179,18 +1252,18 @@ Reference to a `BearerTokenResolver` which will retrieve the bearer token from t Reference to a `AuthenticationEntryPoint` which will handle unauthorized requests [[nsa-jwt]] -=== +== Represents an OAuth 2.0 Resource Server that will authorize JWTs [[nsa-jwt-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-jwt-attributes]] -==== Attributes +=== Attributes [[nsa-jwt-jwt-authentication-converter-ref]] * **jwt-authentication-converter-ref** @@ -1205,16 +1278,16 @@ Reference to a `JwtDecoder`. This is a larger component that overrides `jwk-set- The JWK Set Uri used to load signing verification keys from an OAuth 2.0 Authorization Server [[nsa-opaque-token]] -=== +== Represents an OAuth 2.0 Resource Server that will authorize opaque tokens [[nsa-opaque-token-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-opaque-token-attributes]] -==== Attributes +=== Attributes [[nsa-opaque-token-introspector-ref]] * **introspector-ref** @@ -1232,14 +1305,256 @@ The Client Id to use for client authentication against the provided `introspecti * **client-secret** The Client Secret to use for client authentication against the provided `introspection-uri`. + +[[nsa-relying-party-registrations]] +== +The container element for relying party(ies) registered (xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[ClientRegistration]) with a SAML 2.0 Identity Provider. + + +[[nsa-relying-party-registrations-children]] +=== Child Elements of + +* <> +* <> + + +[[nsa-relying-party-registration]] +== +Represents a relying party registered with a SAML 2.0 Identity Provider + + +[[nsa-relying-party-registration-parents]] +=== Parent Elements of + +* <> + + +[[nsa-relying-party-registration-attributes]] +=== Attributes + + +[[nsa-relying-party-registration-registration-id]] +* **registration-id** +The ID that uniquely identifies the `RelyingPartyRegistration`. + +[[nsa-relying-party-registration-metadata-location]] +* **metadata-location** +The asserting party metadata location. + +[[nsa-relying-party-registration-entity-id]] +* **client-id** +The relying party's https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor[EntityID]. + + +[[nsa-relying-party-registration-assertion-consumer-service-location]] +* **assertion-consumer-service-location** +The AssertionConsumerService Location. Equivalent to the value found in `<AssertionConsumerService Location="..."/>` in the relying party's `<SPSSODescriptor>`. + + +[[nsa-relying-party-registration-assertion-consumer-service-binding]] +* **assertion-consumer-service-binding** +the AssertionConsumerService Binding. Equivalent to the value found in `<AssertionConsumerService Binding="..."/>` in the relying party's `<SPSSODescriptor>`. +The supported values are *POST* and *REDIRECT*. + +[[nsa-relying-party-registration-single-logout-service-location]] +* **single-logout-service-location** +The SingleLogoutService Location. Equivalent to the value found in <SingleLogoutService Location="..."/> in the relying party's <SPSSODescriptor>. + +[[nsa-relying-party-registration-single-logout-service-response-location]] +* **single-logout-service-response-location** +The SingleLogoutService ResponseLocation. Equivalent to the value found in <SingleLogoutService ResponseLocation="..."/> in the relying party's <SPSSODescriptor>. + +[[nsa-relying-party-registration-single-logout-service-binding]] +* **single-logout-service-binding** +The SingleLogoutService Binding. Equivalent to the value found in <SingleLogoutService Binding="..."/> in the relying party's <SPSSODescriptor>. +The supported values are *POST* and *REDIRECT*. + +[[nsa-relying-party-registration-asserting-party-id]] +* **asserting-party-id** +A reference to the associated asserting party. Must reference an `` element. + +[[nsa-relying-party-registration-children]] +=== Child Elements of + +* <> +* <> + + +[[nsa-decryption-credential]] +== +The decryption credentials associated with the relying party. + + +[[nsa-decryption-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-decryption-credential-attributes]] +=== Attributes + + +[[nsa-decryption-credential-certificate-location]] +* **certificate-location** +The location to get the certificate + +[[nsa-decryption-credential-private-key-location]] +* **private-key-location** +The location to get the Relying Party's private key + + +[[nsa-signing-credential]] +== +The signing credentials associated with the relying party. + + +[[nsa-signing-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-signing-credential-attributes]] +=== Attributes + + +[[nsa-signing-credential-certificate-location]] +* **certificate-location** +The location to get this certificate + +[[nsa-signing-credential-private-key-location]] +* **private-key-location** +The location to get the Relying Party's private key + + + +[[nsa-asserting-party]] +== +The configuration information for a SAML 2.0 Asserting Party. + + +[[nsa-asserting-party-parents]] +=== Parent Elements of + +* <> + + +[[nsa-asserting-party-attributes]] +=== Attributes + + +[[nsa-asserting-party-asserting-party-id]] +* **asserting-party-id** +The ID that uniquely identifies the asserting party. + + +[[nsa-asserting-party-entity-id]] +* **entity-id** +The EntityID of the Asserting Party + + +[[nsa-asserting-party-want-authn-requests-signed]] +* **want-authn-requests-signed** +The `WantAuthnRequestsSigned` setting, indicating the asserting party's preference that relying parties should sign the `AuthnRequest` before sending. + + +[[nsa-asserting-party-single-sign-on-service-location]] +* **single-sign-on-service-location** +The https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint[SingleSignOnService] Location. + + +[[nsa-asserting-party-single-sign-on-service-binding]] +* **single-sign-on-service-binding** +The https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint[SingleSignOnService] Binding. +The supported values are *POST* and *REDIRECT*. + + +[[nsa-asserting-party-signing-algorithms]] +* **signing-algorithms** +The list of `org.opensaml.saml.ext.saml2alg.SigningMethod` Algorithms for this asserting party, in preference order. + + +[[nsa-asserting-party-single-logout-service-location]] +* **single-logout-service-location** +The SingleLogoutService Location. Equivalent to the value found in <SingleLogoutService Location="..."/> in the asserting party's <IDPSSODescriptor>. + + +[[nsa-asserting-party-single-logout-service-response-location]] +* **single-logout-service-response-location** +The SingleLogoutService ResponseLocation. Equivalent to the value found in <SingleLogoutService ResponseLocation="..."/> in the asserting party's <IDPSSODescriptor>. + + +[[nsa-asserting-party-single-logout-service-binding]] +* **single-logout-service-binding** +The SingleLogoutService Binding. Equivalent to the value found in <SingleLogoutService Binding="..."/> in the asserting party's <IDPSSODescriptor>. +The supported values are *POST* and *REDIRECT*. + + +[[nsa-asserting-party-children]] +=== Child Elements of + +* <> +* <> + + +[[nsa-encryption-credential]] +== +The encryption credentials associated with the asserting party. + + +[[nsa-encryption-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-encryption-credential-attributes]] +=== Attributes + + +[[nsa-encryption-credential-certificate-location]] +* **certificate-location** +The location to get the certificate + +[[nsa-encryption-credential-private-key-location]] +* **private-key-location** +The location to get the Relying Party's private key + + +[[nsa-verification-credential]] +== +The verification credentials associated with the asserting party. + + +[[nsa-verification-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-verification-credential-attributes]] +=== Attributes + + +[[nsa-verification-credential-certificate-location]] +* **certificate-location** +The location to get this certificate + +[[nsa-verification-credential-private-key-location]] +* **private-key-location** +The location to get the Relying Party's private key + + + [[nsa-http-basic]] -=== +== Adds a `BasicAuthenticationFilter` and `BasicAuthenticationEntryPoint` to the configuration. The latter will only be used as the configuration entry point if form-based login is not enabled. [[nsa-http-basic-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1247,7 +1562,7 @@ The latter will only be used as the configuration entry point if form-based logi [[nsa-http-basic-attributes]] -==== Attributes +=== Attributes [[nsa-http-basic-authentication-details-source-ref]] @@ -1261,13 +1576,13 @@ Sets the `AuthenticationEntryPoint` which is used by the `BasicAuthenticationFil [[nsa-http-firewall]] -=== Element +== Element This is a top-level element which can be used to inject a custom implementation of `HttpFirewall` into the `FilterChainProxy` created by the namespace. The default implementation should be suitable for most applications. [[nsa-http-firewall-attributes]] -==== Attributes +=== Attributes [[nsa-http-firewall-ref]] @@ -1276,7 +1591,7 @@ Defines a reference to a Spring bean that implements `HttpFirewall`. [[nsa-intercept-url]] -=== +== This element is used to define the set of URL patterns that the application is interested in and to configure how they should be handled. It is used to construct the `FilterInvocationSecurityMetadataSource` used by the `FilterSecurityInterceptor`. It is also responsible for configuring a `ChannelProcessingFilter` if particular URLs need to be accessed by HTTPS, for example. @@ -1285,7 +1600,7 @@ So the most specific patterns should come first and the most general should come [[nsa-intercept-url-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1294,7 +1609,7 @@ So the most specific patterns should come first and the most general should come [[nsa-intercept-url-attributes]] -==== Attributes +=== Attributes [[nsa-intercept-url-access]] @@ -1341,12 +1656,12 @@ NOTE: This property is invalid for < +== Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. [[nsa-jee-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1354,7 +1669,7 @@ Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integ [[nsa-jee-attributes]] -==== Attributes +=== Attributes [[nsa-jee-mappable-roles]] @@ -1368,13 +1683,13 @@ A reference to a user-service (or UserDetailsService bean) Id [[nsa-logout]] -=== +== Adds a `LogoutFilter` to the filter stack. This is configured with a `SecurityContextLogoutHandler`. [[nsa-logout-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1382,7 +1697,7 @@ This is configured with a `SecurityContextLogoutHandler`. [[nsa-logout-attributes]] -==== Attributes +=== Attributes [[nsa-logout-delete-cookies]] @@ -1419,7 +1734,7 @@ May be used to supply an instance of `LogoutSuccessHandler` which will be invoke [[nsa-openid-login]] -=== +== Similar to `` and has the same attributes. The default value for `login-processing-url` is "/login/openid". An `OpenIDAuthenticationFilter` and `OpenIDAuthenticationProvider` will be registered. @@ -1428,7 +1743,7 @@ Again, this can be specified by `id`, using the `user-service-ref` attribute, or [[nsa-openid-login-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1436,7 +1751,7 @@ Again, this can be specified by `id`, using the `user-service-ref` attribute, or [[nsa-openid-login-attributes]] -==== Attributes +=== Attributes [[nsa-openid-login-always-use-default-target]] @@ -1514,13 +1829,132 @@ Defaults to "username". [[nsa-openid-login-children]] -==== Child Elements of +=== Child Elements of * <> +[[nsa-saml2-login]] +== +The xref:servlet/saml2/login/index.adoc#servlet-saml2login[SAML 2.0 Login] feature configures authentication support using an SAML 2.0 Service Provider. + + +[[nsa-saml2-login-parents]] +=== Parent Elements of + +* <> + +[[nsa-saml2-login-attributes]] +=== Attributes + + +[[nsa-saml2-login-relying-party-registration-repository-ref]] +* **relying-party-registration-repository-ref** +Reference to the `RelyingPartyRegistrationRepository`. + + +[[nsa-saml2-login-authentication-request-repository-ref]] +* **authentication-request-repository-ref** +Reference to the `Saml2AuthenticationRequestRepository`. + + +[[nsa-saml2-login-authentication-request-resolver-ref]] +* **authentication-request-context-resolver-ref** +Reference to the `Saml2AuthenticationRequestResolver`. + + +[[nsa-saml2-login-authentication-converter-ref]] +* **authentication-converter-ref** +Reference to the `AuthenticationConverter`. + + +[[nsa-saml2-login-login-processing-url]] +* **login-processing-url** +The URI where the filter processes authentication requests. + + +[[nsa-saml2-login-login-page]] +* **login-page** +The URI to send users to login. + + +[[nsa-saml2-login-authentication-success-handler-ref]] +* **authentication-success-handler-ref** +Reference to the `AuthenticationSuccessHandler`. + + +[[nsa-saml2-login-authentication-failure-handler-ref]] +* **authentication-failure-handler-ref** +Reference to the `AuthenticationFailureHandler`. + + +[[nsa-saml2-login-authentication-manager-ref]] +* **authentication-manager-ref** +Reference to the `AuthenticationManager`. + + + +[[nsa-saml2-logout]] +== +The xref:servlet/saml2/logout.adoc#servlet-saml2login-logout[SAML 2.0 Single Logout] feature configures support for RP- and AP-initiated SAML 2.0 Single Logout. + + +[[nsa-saml2-logout-parents]] +=== Parent Elements of + +* <> + +[[nsa-saml2-logout-attributes]] +=== Attributes + + +[[nsa-saml2-logout-logout-url]] +* **logout-url** +The URL by which the relying or asserting party can trigger logout. + + +[[nsa-saml2-logout-logout-request-url]] +* **logout-request-url** +The URL by which the asserting party can send a SAML 2.0 Logout Request. + + +[[nsa-saml2-logout-logout-response-url]] +* **logout-response-url** +The URL by which the asserting party can send a SAML 2.0 Logout Response. + + +[[nsa-saml2-logout-relying-party-registration-repository-ref]] +* **relying-party-registration-repository-ref** +Reference to the `RelyingPartyRegistrationRepository`. + + +[[nsa-saml2-logout-logout-request-validator-ref]] +* **logout-request-validator-ref** +Reference to the `Saml2LogoutRequestValidator`. + + +[[nsa-saml2-logout-logout-request-resolver-ref]] +* **logout-request-resolver-ref** +Reference to the `Saml2LogoutRequestResolver`. + + +[[nsa-saml2-logout-logout-request-repository-ref]] +* **logout-request-repository-ref** +Reference to the `Saml2LogoutRequestRepository`. + + +[[nsa-saml2-logout-logout-response-validator-ref]] +* **logout-response-validator-ref** +Reference to the `Saml2LogoutResponseValidator`. + + +[[nsa-saml2-logout-logout-response-resolver-ref]] +* **logout-response-resolver-ref** +Reference to the `Saml2LogoutResponseResolver`. + + [[nsa-attribute-exchange]] -=== +== The `attribute-exchange` element defines the list of attributes which should be requested from the identity provider. An example can be found in the xref:servlet/authentication/openid.adoc#servlet-openid[OpenID Support] section of the namespace configuration chapter. More than one can be used, in which case each must have an `identifier-match` attribute, containing a regular expression which is matched against the supplied OpenID identifier. @@ -1528,7 +1962,7 @@ This allows different attribute lists to be fetched from different providers (Go [[nsa-attribute-exchange-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1536,7 +1970,7 @@ This allows different attribute lists to be fetched from different providers (Go [[nsa-attribute-exchange-attributes]] -==== Attributes +=== Attributes [[nsa-attribute-exchange-identifier-match]] @@ -1545,7 +1979,7 @@ A regular expression which will be compared against the claimed identity, when d [[nsa-attribute-exchange-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1553,12 +1987,12 @@ A regular expression which will be compared against the claimed identity, when d [[nsa-openid-attribute]] -=== +== Attributes used when making an OpenID AX https://openid.net/specs/openid-attribute-exchange-1_0.html#fetch_request[ Fetch Request] [[nsa-openid-attribute-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1566,7 +2000,7 @@ Attributes used when making an OpenID AX https://openid.net/specs/openid-attribu [[nsa-openid-attribute-attributes]] -==== Attributes +=== Attributes [[nsa-openid-attribute-count]] @@ -1595,23 +2029,23 @@ For example, https://axschema.org/contact/email. See your OP's documentation for valid attribute types. [[nsa-password-management]] -=== +== This element configures password management. [[nsa-password-management-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-password-management-attributes]] -==== Attributes +=== Attributes [[nsa-password-management-change-password-page]] * **change-password-page** The change password page. Defaults to "/change-password". [[nsa-port-mappings]] -=== +== By default, an instance of `PortMapperImpl` will be added to the configuration for use in redirecting to secure and insecure URLs. This element can optionally be used to override the default mappings which that class defines. Each child `` element defines a pair of HTTP:HTTPS ports. @@ -1620,7 +2054,7 @@ An example of overriding these can be found in xref:servlet/exploits/http.adoc#s [[nsa-port-mappings-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1628,7 +2062,7 @@ An example of overriding these can be found in xref:servlet/exploits/http.adoc#s [[nsa-port-mappings-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1636,12 +2070,12 @@ An example of overriding these can be found in xref:servlet/exploits/http.adoc#s [[nsa-port-mapping]] -=== +== Provides a method to map http ports to https ports when forcing a redirect. [[nsa-port-mapping-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1649,7 +2083,7 @@ Provides a method to map http ports to https ports when forcing a redirect. [[nsa-port-mapping-attributes]] -==== Attributes +=== Attributes [[nsa-port-mapping-http]] @@ -1663,13 +2097,13 @@ The https port to use. [[nsa-remember-me]] -=== +== Adds the `RememberMeAuthenticationFilter` to the stack. This in turn will be configured with either a `TokenBasedRememberMeServices`, a `PersistentTokenBasedRememberMeServices` or a user-specified bean implementing `RememberMeServices` depending on the attribute settings. [[nsa-remember-me-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1677,7 +2111,7 @@ This in turn will be configured with either a `TokenBasedRememberMeServices`, a [[nsa-remember-me-attributes]] -==== Attributes +=== Attributes [[nsa-remember-me-authentication-success-handler-ref]] @@ -1757,17 +2191,17 @@ If there are multiple instances, you can specify a bean `id` explicitly using th [[nsa-request-cache]] -=== Element +== Element Sets the `RequestCache` instance which will be used by the `ExceptionTranslationFilter` to store request information before invoking an `AuthenticationEntryPoint`. [[nsa-request-cache-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-request-cache-attributes]] -==== Attributes +=== Attributes [[nsa-request-cache-ref]] @@ -1776,12 +2210,12 @@ Defines a reference to a Spring bean that is a `RequestCache`. [[nsa-session-management]] -=== +== Session-management related functionality is implemented by the addition of a `SessionManagementFilter` to the filter stack. [[nsa-session-management-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1789,7 +2223,7 @@ Session-management related functionality is implemented by the addition of a `Se [[nsa-session-management-attributes]] -==== Attributes +=== Attributes [[nsa-session-management-invalid-session-url]] @@ -1831,7 +2265,7 @@ See the Javadoc for this class for more details. [[nsa-session-management-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1839,7 +2273,7 @@ See the Javadoc for this class for more details. [[nsa-concurrency-control]] -=== +== Adds support for concurrent session control, allowing limits to be placed on the number of active sessions a user can have. A `ConcurrentSessionFilter` will be created, and a `ConcurrentSessionControlAuthenticationStrategy` will be used with the `SessionManagementFilter`. If a `form-login` element has been declared, the strategy object will also be injected into the created authentication filter. @@ -1847,7 +2281,7 @@ An instance of `SessionRegistry` (a `SessionRegistryImpl` instance unless the us [[nsa-concurrency-control-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1855,7 +2289,7 @@ An instance of `SessionRegistry` (a `SessionRegistryImpl` instance unless the us [[nsa-concurrency-control-attributes]] -==== Attributes +=== Attributes [[nsa-concurrency-control-error-if-maximum-exceeded]] @@ -1893,7 +2327,7 @@ The other concurrent session control beans will be wired up to use it. [[nsa-x509]] -=== +== Adds support for X.509 authentication. An `X509AuthenticationFilter` will be added to the stack and an `Http403ForbiddenEntryPoint` bean will be created. The latter will only be used if no other authentication mechanisms are in use (its only functionality is to return an HTTP 403 error code). @@ -1901,7 +2335,7 @@ A `PreAuthenticatedAuthenticationProvider` will also be created which delegates [[nsa-x509-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1909,7 +2343,7 @@ A `PreAuthenticatedAuthenticationProvider` will also be created which delegates [[nsa-x509-attributes]] -==== Attributes +=== Attributes [[nsa-x509-authentication-details-source-ref]] @@ -1929,12 +2363,12 @@ If not set, an attempt will be made to locate a suitable instance automatically [[nsa-filter-chain-map]] -=== +== Used to explicitly configure a FilterChainProxy instance with a FilterChainMap [[nsa-filter-chain-map-attributes]] -==== Attributes +=== Attributes [[nsa-filter-chain-map-request-matcher]] @@ -1944,7 +2378,7 @@ Currently the options are 'ant' (for ant path patterns), 'regex' for regular exp [[nsa-filter-chain-map-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1952,13 +2386,13 @@ Currently the options are 'ant' (for ant path patterns), 'regex' for regular exp [[nsa-filter-chain]] -=== +== Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. [[nsa-filter-chain-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1966,7 +2400,7 @@ When multiple filter-chain elements are assembled in a list in order to configur [[nsa-filter-chain-attributes]] -==== Attributes +=== Attributes [[nsa-filter-chain-filters]] @@ -1986,7 +2420,7 @@ A reference to a `RequestMatcher` that will be used to determine if any `Filter` [[nsa-filter-security-metadata-source]] -=== +== Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. @@ -1994,7 +2428,7 @@ Any others will result in a configuration error. [[nsa-filter-security-metadata-source-attributes]] -==== Attributes +=== Attributes [[nsa-filter-security-metadata-source-id]] @@ -2017,1011 +2451,7 @@ If the expression evaluates to 'true', access will be granted. [[nsa-filter-security-metadata-source-children]] -==== Child Elements of +=== Child Elements of * <> - -[[nsa-websocket-security]] -== WebSocket Security - -Spring Security 4.0+ provides support for authorizing messages. -One concrete example of where this is useful is to provide authorization in WebSocket based applications. - -[[nsa-websocket-message-broker]] -=== - -The websocket-message-broker element has two different modes. -If the <> is not specified, then it will do the following things: - -* Ensure that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver. -This allows the use of `@AuthenticationPrincipal` to resolve the principal of the current `Authentication` -* Ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel. -This populates the SecurityContextHolder with the user that is found in the Message -* Ensures that a ChannelSecurityInterceptor is registered with the clientInboundChannel. -This allows authorization rules to be specified for a message. -* Ensures that a CsrfChannelInterceptor is registered with the clientInboundChannel. -This ensures that only requests from the original domain are enabled. -* Ensures that a CsrfTokenHandshakeInterceptor is registered with WebSocketHttpRequestHandler, TransportHandlingSockJsService, or DefaultSockJsService. -This ensures that the expected CsrfToken from the HttpServletRequest is copied into the WebSocket Session attributes. - -If additional control is necessary, the id can be specified and a ChannelSecurityInterceptor will be assigned to the specified id. -All the wiring with Spring's messaging infrastructure can then be done manually. -This is more cumbersome, but provides greater control over the configuration. - - -[[nsa-websocket-message-broker-attributes]] -==== Attributes - -[[nsa-websocket-message-broker-id]] -* **id** A bean identifier, used for referring to the ChannelSecurityInterceptor bean elsewhere in the context. -If specified, Spring Security requires explicit configuration within Spring Messaging. -If not specified, Spring Security will automatically integrate with the messaging infrastructure as described in <> - -[[nsa-websocket-message-broker-same-origin-disabled]] -* **same-origin-disabled** Disables the requirement for CSRF token to be present in the Stomp headers (default false). -Changing the default is useful if it is necessary to allow other origins to make SockJS connections. - -[[nsa-websocket-message-broker-children]] -==== Child Elements of - - -* <> -* <> - -[[nsa-intercept-message]] -=== - -Defines an authorization rule for a message. - - -[[nsa-intercept-message-parents]] -==== Parent Elements of - - -* <> - - -[[nsa-intercept-message-attributes]] -==== Attributes - -[[nsa-intercept-message-pattern]] -* **pattern** An ant based pattern that matches on the Message destination. -For example, "/**" matches any Message with a destination; "/admin/**" matches any Message that has a destination that starts with "/admin/**". - -[[nsa-intercept-message-type]] -* **type** The type of message to match on. -Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). - -[[nsa-intercept-message-access]] -* **access** The expression used to secure the Message. -For example, "denyAll" will deny access to all of the matching Messages; "permitAll" will grant access to all of the matching Messages; "hasRole('ADMIN') requires the current user to have the role 'ROLE_ADMIN' for the matching Messages. - -[[nsa-authentication]] -== Authentication Services -Before Spring Security 3.0, an `AuthenticationManager` was automatically registered internally. -Now you must register one explicitly using the `` element. -This creates an instance of Spring Security's `ProviderManager` class, which needs to be configured with a list of one or more `AuthenticationProvider` instances. -These can either be created using syntax elements provided by the namespace, or they can be standard bean definitions, marked for addition to the list using the `authentication-provider` element. - - -[[nsa-authentication-manager]] -=== -Every Spring Security application which uses the namespace must have include this element somewhere. -It is responsible for registering the `AuthenticationManager` which provides authentication services to the application. -All elements which create `AuthenticationProvider` instances should be children of this element. - - -[[nsa-authentication-manager-attributes]] -==== Attributes - - -[[nsa-authentication-manager-alias]] -* **alias** -This attribute allows you to define an alias name for the internal instance for use in your own configuration. - - -[[nsa-authentication-manager-erase-credentials]] -* **erase-credentials** -If set to true, the AuthenticationManager will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. -Literally it maps to the `eraseCredentialsAfterAuthentication` property of the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. - - -[[nsa-authentication-manager-id]] -* **id** -This attribute allows you to define an id for the internal instance for use in your own configuration. -It is the same as the alias element, but provides a more consistent experience with elements that use the id attribute. - - -[[nsa-authentication-manager-children]] -==== Child Elements of - - -* <> -* <> - - - -[[nsa-authentication-provider]] -=== -Unless used with a `ref` attribute, this element is shorthand for configuring a `DaoAuthenticationProvider`. -`DaoAuthenticationProvider` loads user information from a `UserDetailsService` and compares the username/password combination with the values supplied at login. -The `UserDetailsService` instance can be defined either by using an available namespace element (`jdbc-user-service` or by using the `user-service-ref` attribute to point to a bean defined elsewhere in the application context). - - - -[[nsa-authentication-provider-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-authentication-provider-attributes]] -==== Attributes - - -[[nsa-authentication-provider-ref]] -* **ref** -Defines a reference to a Spring bean that implements `AuthenticationProvider`. - -If you have written your own `AuthenticationProvider` implementation (or want to configure one of Spring Security's own implementations as a traditional bean for some reason, then you can use the following syntax to add it to the internal list of `ProviderManager`: - -[source,xml] ----- - - - - - - ----- - - - - -[[nsa-authentication-provider-user-service-ref]] -* **user-service-ref** -A reference to a bean that implements UserDetailsService that may be created using the standard bean element or the custom user-service element. - - -[[nsa-authentication-provider-children]] -==== Child Elements of - - -* <> -* <> -* <> -* <> - - - -[[nsa-jdbc-user-service]] -=== -Causes creation of a JDBC-based UserDetailsService. - - -[[nsa-jdbc-user-service-attributes]] -==== Attributes - - -[[nsa-jdbc-user-service-authorities-by-username-query]] -* **authorities-by-username-query** -An SQL statement to query for a user's granted authorities given a username. - -The default is - -[source] ----- -select username, authority from authorities where username = ? ----- - - - - -[[nsa-jdbc-user-service-cache-ref]] -* **cache-ref** -Defines a reference to a cache for use with a UserDetailsService. - - -[[nsa-jdbc-user-service-data-source-ref]] -* **data-source-ref** -The bean ID of the DataSource which provides the required tables. - - -[[nsa-jdbc-user-service-group-authorities-by-username-query]] -* **group-authorities-by-username-query** -An SQL statement to query user's group authorities given a username. -The default is - -+ - -[source] ----- -select -g.id, g.group_name, ga.authority -from -groups g, group_members gm, group_authorities ga -where -gm.username = ? and g.id = ga.group_id and g.id = gm.group_id ----- - - - - -[[nsa-jdbc-user-service-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-jdbc-user-service-role-prefix]] -* **role-prefix** -A non-empty string prefix that will be added to role strings loaded from persistent storage (default is "ROLE_"). -Use the value "none" for no prefix in cases where the default is non-empty. - - -[[nsa-jdbc-user-service-users-by-username-query]] -* **users-by-username-query** -An SQL statement to query a username, password, and enabled status given a username. -The default is - -+ - -[source] ----- -select username, password, enabled from users where username = ? ----- - - - - -[[nsa-password-encoder]] -=== -Authentication providers can optionally be configured to use a password encoder as described in the xref:features/authentication/password-storage.adoc#authentication-password-storage[Password Storage]. -This will result in the bean being injected with the appropriate `PasswordEncoder` instance. - - -[[nsa-password-encoder-parents]] -==== Parent Elements of - - -* <> -* <> - - - -[[nsa-password-encoder-attributes]] -==== Attributes - - -[[nsa-password-encoder-hash]] -* **hash** -Defines the hashing algorithm used on user passwords. -We recommend strongly against using MD4, as it is a very weak hashing algorithm. - - -[[nsa-password-encoder-ref]] -* **ref** -Defines a reference to a Spring bean that implements `PasswordEncoder`. - - -[[nsa-user-service]] -=== -Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. -Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. - - -[[nsa-user-service-attributes]] -==== Attributes - - -[[nsa-user-service-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-user-service-properties]] -* **properties** -The location of a Properties file where each line is in the format of - -+ - -[source] ----- -username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] ----- - - - - -[[nsa-user-service-children]] -==== Child Elements of - - -* <> - - - -[[nsa-user]] -=== -Represents a user in the application. - - -[[nsa-user-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-user-attributes]] -==== Attributes - - -[[nsa-user-authorities]] -* **authorities** -One of more authorities granted to the user. -Separate authorities with a comma (but no space). -For example, "ROLE_USER,ROLE_ADMINISTRATOR" - - -[[nsa-user-disabled]] -* **disabled** -Can be set to "true" to mark an account as disabled and unusable. - - -[[nsa-user-locked]] -* **locked** -Can be set to "true" to mark an account as locked and unusable. - - -[[nsa-user-name]] -* **name** -The username assigned to the user. - - -[[nsa-user-password]] -* **password** -The password assigned to the user. -This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). -This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. -If omitted, the namespace will generate a random value, preventing its accidental use for authentication. -Cannot be empty. - - - -== Method Security - -[[nsa-method-security]] -=== -This element is the primary means of adding support for securing methods on Spring Security beans. -Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts. - -[[nsa-method-security-attributes]] -==== attributes - -[[nsa-method-security-pre-post-enabled]] -* **pre-post-enabled** -Enables Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) for this application context. -Defaults to "true". - -[[nsa-method-security-secured-enabled]] -* **secured-enabled** -Enables Spring Security's @Secured annotation for this application context. -Defaults to "false". - -[[nsa-method-security-jsr250-enabled]] -* **jsr250-enabled** -Enables JSR-250 authorization annotations (@RolesAllowed, @PermitAll, @DenyAll) for this application context. -Defaults to "false". - -[[nsa-method-security-proxy-target-class]] -* **proxy-target-class** -If true, class based proxying will be used instead of interface based proxying. -Defaults to "false". - -[[nsa-method-security-children]] -==== Child Elements of - -* <> - -[[nsa-global-method-security]] -=== -This element is the primary means of adding support for securing methods on Spring Security beans. -Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts as child elements, using AspectJ syntax. - - -[[nsa-global-method-security-attributes]] -==== Attributes - - -[[nsa-global-method-security-access-decision-manager-ref]] -* **access-decision-manager-ref** -Method security uses the same `AccessDecisionManager` configuration as web security, but this can be overridden using this attribute. -By default an AffirmativeBased implementation is used for with a RoleVoter and an AuthenticatedVoter. - - -[[nsa-global-method-security-authentication-manager-ref]] -* **authentication-manager-ref** -A reference to an `AuthenticationManager` that should be used for method security. - - -[[nsa-global-method-security-jsr250-annotations]] -* **jsr250-annotations** -Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). -This will require the javax.annotation.security classes on the classpath. -Setting this to true also adds a `Jsr250Voter` to the `AccessDecisionManager`, so you need to make sure you do this if you are using a custom implementation and want to use these annotations. - - -[[nsa-global-method-security-metadata-source-ref]] -* **metadata-source-ref** -An external `MethodSecurityMetadataSource` instance can be supplied which will take priority over other sources (such as the default annotations). - - -[[nsa-global-method-security-mode]] -* **mode** -This attribute can be set to "aspectj" to specify that AspectJ should be used instead of the default Spring AOP. -Secured methods must be woven with the `AnnotationSecurityAspect` from the `spring-security-aspects` module. - -It is important to note that AspectJ follows Java's rule that annotations on interfaces are not inherited. -This means that methods that define the Security annotations on the interface will not be secured. -Instead, you must place the Security annotation on the class when using AspectJ. - - -[[nsa-global-method-security-order]] -* **order** -Allows the advice "order" to be set for the method security interceptor. - - -[[nsa-global-method-security-pre-post-annotations]] -* **pre-post-annotations** -Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. -Defaults to "disabled". - - -[[nsa-global-method-security-proxy-target-class]] -* **proxy-target-class** -If true, class based proxying will be used instead of interface based proxying. - - -[[nsa-global-method-security-run-as-manager-ref]] -* **run-as-manager-ref** -A reference to an optional `RunAsManager` implementation which will be used by the configured `MethodSecurityInterceptor` - - -[[nsa-global-method-security-secured-annotations]] -* **secured-annotations** -Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. -Defaults to "disabled". - - -[[nsa-global-method-security-children]] -==== Child Elements of - - -* <> -* <> -* <> -* <> - - - -[[nsa-after-invocation-provider]] -=== -This element can be used to decorate an `AfterInvocationProvider` for use by the security interceptor maintained by the `` namespace. -You can define zero or more of these within the `global-method-security` element, each with a `ref` attribute pointing to an `AfterInvocationProvider` bean instance within your application context. - - -[[nsa-after-invocation-provider-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-after-invocation-provider-attributes]] -==== Attributes - - -[[nsa-after-invocation-provider-ref]] -* **ref** -Defines a reference to a Spring bean that implements `AfterInvocationProvider`. - - -[[nsa-pre-post-annotation-handling]] -=== -Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replaced entirely. -Only applies if these annotations are enabled. - - -[[nsa-pre-post-annotation-handling-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-pre-post-annotation-handling-children]] -==== Child Elements of - - -* <> -* <> -* <> - - - -[[nsa-invocation-attribute-factory]] -=== -Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. - - -[[nsa-invocation-attribute-factory-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-invocation-attribute-factory-attributes]] -==== Attributes - - -[[nsa-invocation-attribute-factory-ref]] -* **ref** -Defines a reference to a Spring bean Id. - - -[[nsa-post-invocation-advice]] -=== -Customizes the `PostInvocationAdviceProvider` with the ref as the `PostInvocationAuthorizationAdvice` for the element. - - -[[nsa-post-invocation-advice-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-post-invocation-advice-attributes]] -==== Attributes - - -[[nsa-post-invocation-advice-ref]] -* **ref** -Defines a reference to a Spring bean Id. - - -[[nsa-pre-invocation-advice]] -=== -Customizes the `PreInvocationAuthorizationAdviceVoter` with the ref as the `PreInvocationAuthorizationAdviceVoter` for the element. - - -[[nsa-pre-invocation-advice-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-pre-invocation-advice-attributes]] -==== Attributes - - -[[nsa-pre-invocation-advice-ref]] -* **ref** -Defines a reference to a Spring bean Id. - - -[[nsa-protect-pointcut]] -=== Securing Methods using -`` -Rather than defining security attributes on an individual method or class basis using the `@Secured` annotation, you can define cross-cutting security constraints across whole sets of methods and interfaces in your service layer using the `` element. -You can find an example in the xref:servlet/authorization/method-security.adoc#ns-protect-pointcut[namespace introduction]. - - -[[nsa-protect-pointcut-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-protect-pointcut-attributes]] -==== Attributes - - -[[nsa-protect-pointcut-access]] -* **access** -Access configuration attributes list that applies to all methods matching the pointcut, e.g. -"ROLE_A,ROLE_B" - - -[[nsa-protect-pointcut-expression]] -* **expression** -An AspectJ expression, including the `execution` keyword. -For example, `execution(int com.foo.TargetObject.countLength(String))`. - - -[[nsa-intercept-methods]] -=== -Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods - - -[[nsa-intercept-methods-attributes]] -==== Attributes - - -[[nsa-intercept-methods-access-decision-manager-ref]] -* **access-decision-manager-ref** -Optional AccessDecisionManager bean ID to be used by the created method security interceptor. - - -[[nsa-intercept-methods-children]] -==== Child Elements of - - -* <> - - - -[[nsa-method-security-metadata-source]] -=== -Creates a MethodSecurityMetadataSource instance - - -[[nsa-method-security-metadata-source-attributes]] -==== Attributes - - -[[nsa-method-security-metadata-source-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-method-security-metadata-source-use-expressions]] -* **use-expressions** -Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. -Defaults to 'false'. -If enabled, each attribute should contain a single Boolean expression. -If the expression evaluates to 'true', access will be granted. - - -[[nsa-method-security-metadata-source-children]] -==== Child Elements of - - -* <> - - - -[[nsa-protect]] -=== -Defines a protected method and the access control configuration attributes that apply to it. -We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". - - -[[nsa-protect-parents]] -==== Parent Elements of - - -* <> -* <> - - - -[[nsa-protect-attributes]] -==== Attributes - - -[[nsa-protect-access]] -* **access** -Access configuration attributes list that applies to the method, e.g. -"ROLE_A,ROLE_B". - - -[[nsa-protect-method]] -* **method** -A method name - - -[[nsa-ldap]] -== LDAP Namespace Options -LDAP is covered in some details in xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[its own chapter]. -We will expand on that here with some explanation of how the namespace options map to Spring beans. -The LDAP implementation uses Spring LDAP extensively, so some familiarity with that project's API may be useful. - - -[[nsa-ldap-server]] -=== Defining the LDAP Server using the -`` Element -This element sets up a Spring LDAP `ContextSource` for use by the other LDAP beans, defining the location of the LDAP server and other information (such as a username and password, if it doesn't allow anonymous access) for connecting to it. -It can also be used to create an embedded server for testing. -Details of the syntax for both options are covered in the xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[LDAP chapter]. -The actual `ContextSource` implementation is `DefaultSpringSecurityContextSource` which extends Spring LDAP's `LdapContextSource` class. -The `manager-dn` and `manager-password` attributes map to the latter's `userDn` and `password` properties respectively. - -If you only have one server defined in your application context, the other LDAP namespace-defined beans will use it automatically. -Otherwise, you can give the element an "id" attribute and refer to it from other namespace beans using the `server-ref` attribute. -This is actually the bean `id` of the `ContextSource` instance, if you want to use it in other traditional Spring beans. - - -[[nsa-ldap-server-attributes]] -==== Attributes - -[[nsa-ldap-server-mode]] -* **mode** -Explicitly specifies which embedded ldap server should use. Values are `apacheds` and `unboundid`. By default, it will depends if the library is available in the classpath. - -[[nsa-ldap-server-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-ldap-server-ldif]] -* **ldif** -Explicitly specifies an ldif file resource to load into an embedded LDAP server. -The ldif should be a Spring resource pattern (i.e. classpath:init.ldif). -The default is classpath*:*.ldif - - -[[nsa-ldap-server-manager-dn]] -* **manager-dn** -Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. -If omitted, anonymous access will be used. - - -[[nsa-ldap-server-manager-password]] -* **manager-password** -The password for the manager DN. -This is required if the manager-dn is specified. - - -[[nsa-ldap-server-port]] -* **port** -Specifies an IP port number. -Used to configure an embedded LDAP server, for example. -The default value is 33389. - - -[[nsa-ldap-server-root]] -* **root** -Optional root suffix for the embedded LDAP server. -Default is "dc=springframework,dc=org" - - -[[nsa-ldap-server-url]] -* **url** -Specifies the ldap server URL when not using the embedded LDAP server. - - -[[nsa-ldap-authentication-provider]] -=== -This element is shorthand for the creation of an `LdapAuthenticationProvider` instance. -By default this will be configured with a `BindAuthenticator` instance and a `DefaultAuthoritiesPopulator`. -As with all namespace authentication providers, it must be included as a child of the `authentication-provider` element. - - -[[nsa-ldap-authentication-provider-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-ldap-authentication-provider-attributes]] -==== Attributes - - -[[nsa-ldap-authentication-provider-group-role-attribute]] -* **group-role-attribute** -The LDAP attribute name which contains the role name which will be used within Spring Security. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupRoleAttribute` property. -Defaults to "cn". - - -[[nsa-ldap-authentication-provider-group-search-base]] -* **group-search-base** -Search base for group membership searches. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchBase` constructor argument. -Defaults to "" (searching from the root). - - -[[nsa-ldap-authentication-provider-group-search-filter]] -* **group-search-filter** -Group search filter. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchFilter` property. -Defaults to `+(uniqueMember={0})+`. -The substituted parameter is the DN of the user. - - -[[nsa-ldap-authentication-provider-role-prefix]] -* **role-prefix** -A non-empty string prefix that will be added to role strings loaded from persistent. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `rolePrefix` property. -Defaults to "ROLE_". -Use the value "none" for no prefix in cases where the default is non-empty. - - -[[nsa-ldap-authentication-provider-server-ref]] -* **server-ref** -The optional server to use. -If omitted, and a default LDAP server is registered (using with no Id), that server will be used. - - -[[nsa-ldap-authentication-provider-user-context-mapper-ref]] -* **user-context-mapper-ref** -Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry - - -[[nsa-ldap-authentication-provider-user-details-class]] -* **user-details-class** -Allows the objectClass of the user entry to be specified. -If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object - - -[[nsa-ldap-authentication-provider-user-dn-pattern]] -* **user-dn-pattern** -If your users are at a fixed location in the directory (i.e. you can work out the DN directly from the username without doing a directory search), you can use this attribute to map directly to the DN. -It maps directly to the `userDnPatterns` property of `AbstractLdapAuthenticator`. -The value is a specific pattern used to build the user's DN, for example `+uid={0},ou=people+`. -The key `+{0}+` must be present and will be substituted with the username. - - -[[nsa-ldap-authentication-provider-user-search-base]] -* **user-search-base** -Search base for user searches. -Defaults to "". -Only used with a 'user-search-filter'. - -+ - -If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. -The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. -If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. - - -[[nsa-ldap-authentication-provider-user-search-filter]] -* **user-search-filter** -The LDAP filter used to search for users (optional). -For example `+(uid={0})+`. -The substituted parameter is the user's login name. - -+ - -If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. -The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. -If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. - - -[[nsa-ldap-authentication-provider-children]] -==== Child Elements of - - -* <> - - - -[[nsa-password-compare]] -=== -This is used as child element to `` and switches the authentication strategy from `BindAuthenticator` to `PasswordComparisonAuthenticator`. - - -[[nsa-password-compare-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-password-compare-attributes]] -==== Attributes - - -[[nsa-password-compare-hash]] -* **hash** -Defines the hashing algorithm used on user passwords. -We recommend strongly against using MD4, as it is a very weak hashing algorithm. - - -[[nsa-password-compare-password-attribute]] -* **password-attribute** -The attribute in the directory which contains the user password. -Defaults to "userPassword". - - -[[nsa-password-compare-children]] -==== Child Elements of - - -* <> - - - -[[nsa-ldap-user-service]] -=== -This element configures an LDAP `UserDetailsService`. -The class used is `LdapUserDetailsService` which is a combination of a `FilterBasedLdapUserSearch` and a `DefaultLdapAuthoritiesPopulator`. -The attributes it supports have the same usage as in ``. - - -[[nsa-ldap-user-service-attributes]] -==== Attributes - - -[[nsa-ldap-user-service-cache-ref]] -* **cache-ref** -Defines a reference to a cache for use with a UserDetailsService. - - -[[nsa-ldap-user-service-group-role-attribute]] -* **group-role-attribute** -The LDAP attribute name which contains the role name which will be used within Spring Security. -Defaults to "cn". - - -[[nsa-ldap-user-service-group-search-base]] -* **group-search-base** -Search base for group membership searches. -Defaults to "" (searching from the root). - - -[[nsa-ldap-user-service-group-search-filter]] -* **group-search-filter** -Group search filter. -Defaults to `+(uniqueMember={0})+`. -The substituted parameter is the DN of the user. - - -[[nsa-ldap-user-service-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-ldap-user-service-role-prefix]] -* **role-prefix** -A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. -"ROLE_"). -Use the value "none" for no prefix in cases where the default is non-empty. - - -[[nsa-ldap-user-service-server-ref]] -* **server-ref** -The optional server to use. -If omitted, and a default LDAP server is registered (using with no Id), that server will be used. - - -[[nsa-ldap-user-service-user-context-mapper-ref]] -* **user-context-mapper-ref** -Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry - - -[[nsa-ldap-user-service-user-details-class]] -* **user-details-class** -Allows the objectClass of the user entry to be specified. -If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object - - -[[nsa-ldap-user-service-user-search-base]] -* **user-search-base** -Search base for user searches. -Defaults to "". -Only used with a 'user-search-filter'. - - -[[nsa-ldap-user-service-user-search-filter]] -* **user-search-filter** -The LDAP filter used to search for users (optional). -For example `+(uid={0})+`. -The substituted parameter is the user's login name. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc new file mode 100644 index 00000000000..f2ec7aca5d4 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc @@ -0,0 +1,9 @@ +[[appendix-namespace]] += The Security Namespace +:page-section-summary-toc: 1 + +This appendix provides a reference to the elements available in the security namespace and information on the underlying beans they create (a knowledge of the individual classes and how they work together is assumed - you can find more information in the project Javadoc and elsewhere in this document). +If you haven't used the namespace before, please read the xref:servlet/configuration/xml-namespace.adoc#ns-config[introductory chapter] on namespace configuration, as this is intended as a supplement to the information there. +Using a good quality XML editor while editing a configuration based on the schema is recommended as this will provide contextual information on which elements and attributes are available as well as comments explaining their purpose. +The namespace is written in https://relaxng.org/[RELAX NG] Compact format and later converted into an XSD schema. +If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc[schema file] directly. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc new file mode 100644 index 00000000000..f3c07e6d767 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc @@ -0,0 +1,291 @@ +[[nsa-ldap]] += LDAP Namespace Options +LDAP is covered in some details in xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[its own chapter]. +We will expand on that here with some explanation of how the namespace options map to Spring beans. +The LDAP implementation uses Spring LDAP extensively, so some familiarity with that project's API may be useful. + + +[[nsa-ldap-server]] +== Defining the LDAP Server using the +`` Element +This element sets up a Spring LDAP `ContextSource` for use by the other LDAP beans, defining the location of the LDAP server and other information (such as a username and password, if it doesn't allow anonymous access) for connecting to it. +It can also be used to create an embedded server for testing. +Details of the syntax for both options are covered in the xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[LDAP chapter]. +The actual `ContextSource` implementation is `DefaultSpringSecurityContextSource` which extends Spring LDAP's `LdapContextSource` class. +The `manager-dn` and `manager-password` attributes map to the latter's `userDn` and `password` properties respectively. + +If you only have one server defined in your application context, the other LDAP namespace-defined beans will use it automatically. +Otherwise, you can give the element an "id" attribute and refer to it from other namespace beans using the `server-ref` attribute. +This is actually the bean `id` of the `ContextSource` instance, if you want to use it in other traditional Spring beans. + + +[[nsa-ldap-server-attributes]] +=== Attributes + +[[nsa-ldap-server-mode]] +* **mode** +Explicitly specifies which embedded ldap server should use. Values are `apacheds` and `unboundid`. By default, it will depends if the library is available in the classpath. + +[[nsa-ldap-server-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-ldap-server-ldif]] +* **ldif** +Explicitly specifies an ldif file resource to load into an embedded LDAP server. +The ldif should be a Spring resource pattern (i.e. classpath:init.ldif). +The default is classpath*:*.ldif + + +[[nsa-ldap-server-manager-dn]] +* **manager-dn** +Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. +If omitted, anonymous access will be used. + + +[[nsa-ldap-server-manager-password]] +* **manager-password** +The password for the manager DN. +This is required if the manager-dn is specified. + + +[[nsa-ldap-server-port]] +* **port** +Specifies an IP port number. +Used to configure an embedded LDAP server, for example. +The default value is 33389. + + +[[nsa-ldap-server-root]] +* **root** +Optional root suffix for the embedded LDAP server. +Default is "dc=springframework,dc=org" + + +[[nsa-ldap-server-url]] +* **url** +Specifies the ldap server URL when not using the embedded LDAP server. + + +[[nsa-ldap-authentication-provider]] +== +This element is shorthand for the creation of an `LdapAuthenticationProvider` instance. +By default this will be configured with a `BindAuthenticator` instance and a `DefaultAuthoritiesPopulator`. +As with all namespace authentication providers, it must be included as a child of the `authentication-provider` element. + + +[[nsa-ldap-authentication-provider-parents]] +=== Parent Elements of + + +* xref:servlet/appendix/namespace/authentication-manager.adoc#nsa-authentication-manager[authentication-manager] + + + +[[nsa-ldap-authentication-provider-attributes]] +=== Attributes + + +[[nsa-ldap-authentication-provider-group-role-attribute]] +* **group-role-attribute** +The LDAP attribute name which contains the role name which will be used within Spring Security. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupRoleAttribute` property. +Defaults to "cn". + + +[[nsa-ldap-authentication-provider-group-search-base]] +* **group-search-base** +Search base for group membership searches. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchBase` constructor argument. +Defaults to "" (searching from the root). + + +[[nsa-ldap-authentication-provider-group-search-filter]] +* **group-search-filter** +Group search filter. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchFilter` property. +Defaults to `+(uniqueMember={0})+`. +The substituted parameter is the DN of the user. + + +[[nsa-ldap-authentication-provider-role-prefix]] +* **role-prefix** +A non-empty string prefix that will be added to role strings loaded from persistent. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `rolePrefix` property. +Defaults to "ROLE_". +Use the value "none" for no prefix in cases where the default is non-empty. + + +[[nsa-ldap-authentication-provider-server-ref]] +* **server-ref** +The optional server to use. +If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + + +[[nsa-ldap-authentication-provider-user-context-mapper-ref]] +* **user-context-mapper-ref** +Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + + +[[nsa-ldap-authentication-provider-user-details-class]] +* **user-details-class** +Allows the objectClass of the user entry to be specified. +If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + + +[[nsa-ldap-authentication-provider-user-dn-pattern]] +* **user-dn-pattern** +If your users are at a fixed location in the directory (i.e. you can work out the DN directly from the username without doing a directory search), you can use this attribute to map directly to the DN. +It maps directly to the `userDnPatterns` property of `AbstractLdapAuthenticator`. +The value is a specific pattern used to build the user's DN, for example `+uid={0},ou=people+`. +The key `+{0}+` must be present and will be substituted with the username. + + +[[nsa-ldap-authentication-provider-user-search-base]] +* **user-search-base** +Search base for user searches. +Defaults to "". +Only used with a 'user-search-filter'. + ++ + +If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. +The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. +If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. + + +[[nsa-ldap-authentication-provider-user-search-filter]] +* **user-search-filter** +The LDAP filter used to search for users (optional). +For example `+(uid={0})+`. +The substituted parameter is the user's login name. + ++ + +If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. +The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. +If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. + + +[[nsa-ldap-authentication-provider-children]] +=== Child Elements of + + +* <> + + + +[[nsa-password-compare]] +== +This is used as child element to `` and switches the authentication strategy from `BindAuthenticator` to `PasswordComparisonAuthenticator`. + + +[[nsa-password-compare-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-password-compare-attributes]] +=== Attributes + + +[[nsa-password-compare-hash]] +* **hash** +Defines the hashing algorithm used on user passwords. +We recommend strongly against using MD4, as it is a very weak hashing algorithm. + + +[[nsa-password-compare-password-attribute]] +* **password-attribute** +The attribute in the directory which contains the user password. +Defaults to "userPassword". + + +[[nsa-password-compare-children]] +=== Child Elements of + + +* xref:servlet/appendix/namespace/authentication-manager.adoc#nsa-password-encoder[password-encoder] + + + +[[nsa-ldap-user-service]] +== +This element configures an LDAP `UserDetailsService`. +The class used is `LdapUserDetailsService` which is a combination of a `FilterBasedLdapUserSearch` and a `DefaultLdapAuthoritiesPopulator`. +The attributes it supports have the same usage as in ``. + + +[[nsa-ldap-user-service-attributes]] +=== Attributes + + +[[nsa-ldap-user-service-cache-ref]] +* **cache-ref** +Defines a reference to a cache for use with a UserDetailsService. + + +[[nsa-ldap-user-service-group-role-attribute]] +* **group-role-attribute** +The LDAP attribute name which contains the role name which will be used within Spring Security. +Defaults to "cn". + + +[[nsa-ldap-user-service-group-search-base]] +* **group-search-base** +Search base for group membership searches. +Defaults to "" (searching from the root). + + +[[nsa-ldap-user-service-group-search-filter]] +* **group-search-filter** +Group search filter. +Defaults to `+(uniqueMember={0})+`. +The substituted parameter is the DN of the user. + + +[[nsa-ldap-user-service-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-ldap-user-service-role-prefix]] +* **role-prefix** +A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. +"ROLE_"). +Use the value "none" for no prefix in cases where the default is non-empty. + + +[[nsa-ldap-user-service-server-ref]] +* **server-ref** +The optional server to use. +If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + + +[[nsa-ldap-user-service-user-context-mapper-ref]] +* **user-context-mapper-ref** +Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + + +[[nsa-ldap-user-service-user-details-class]] +* **user-details-class** +Allows the objectClass of the user entry to be specified. +If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + + +[[nsa-ldap-user-service-user-search-base]] +* **user-search-base** +Search base for user searches. +Defaults to "". +Only used with a 'user-search-filter'. + + +[[nsa-ldap-user-service-user-search-filter]] +* **user-search-filter** +The LDAP filter used to search for users (optional). +For example `+(uid={0})+`. +The substituted parameter is the user's login name. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc new file mode 100644 index 00000000000..3d50d5507cb --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc @@ -0,0 +1,340 @@ += Method Security + +[[nsa-method-security]] +== +This element is the primary means of adding support for securing methods on Spring Security beans. +Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts. + +[[nsa-method-security-attributes]] +=== attributes + +[[nsa-method-security-pre-post-enabled]] +* **pre-post-enabled** +Enables Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) for this application context. +Defaults to "true". + +[[nsa-method-security-secured-enabled]] +* **secured-enabled** +Enables Spring Security's @Secured annotation for this application context. +Defaults to "false". + +[[nsa-method-security-jsr250-enabled]] +* **jsr250-enabled** +Enables JSR-250 authorization annotations (@RolesAllowed, @PermitAll, @DenyAll) for this application context. +Defaults to "false". + +[[nsa-method-security-proxy-target-class]] +* **proxy-target-class** +If true, class based proxying will be used instead of interface based proxying. +Defaults to "false". + +[[nsa-method-security-children]] +=== Child Elements of + +* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] + +[[nsa-global-method-security]] +== +This element is the primary means of adding support for securing methods on Spring Security beans. +Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts as child elements, using AspectJ syntax. + + +[[nsa-global-method-security-attributes]] +=== Attributes + + +[[nsa-global-method-security-access-decision-manager-ref]] +* **access-decision-manager-ref** +Method security uses the same `AccessDecisionManager` configuration as web security, but this can be overridden using this attribute. +By default an AffirmativeBased implementation is used for with a RoleVoter and an AuthenticatedVoter. + + +[[nsa-global-method-security-authentication-manager-ref]] +* **authentication-manager-ref** +A reference to an `AuthenticationManager` that should be used for method security. + + +[[nsa-global-method-security-jsr250-annotations]] +* **jsr250-annotations** +Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). +This will require the javax.annotation.security classes on the classpath. +Setting this to true also adds a `Jsr250Voter` to the `AccessDecisionManager`, so you need to make sure you do this if you are using a custom implementation and want to use these annotations. + + +[[nsa-global-method-security-metadata-source-ref]] +* **metadata-source-ref** +An external `MethodSecurityMetadataSource` instance can be supplied which will take priority over other sources (such as the default annotations). + + +[[nsa-global-method-security-mode]] +* **mode** +This attribute can be set to "aspectj" to specify that AspectJ should be used instead of the default Spring AOP. +Secured methods must be woven with the `AnnotationSecurityAspect` from the `spring-security-aspects` module. + +It is important to note that AspectJ follows Java's rule that annotations on interfaces are not inherited. +This means that methods that define the Security annotations on the interface will not be secured. +Instead, you must place the Security annotation on the class when using AspectJ. + + +[[nsa-global-method-security-order]] +* **order** +Allows the advice "order" to be set for the method security interceptor. + + +[[nsa-global-method-security-pre-post-annotations]] +* **pre-post-annotations** +Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. +Defaults to "disabled". + + +[[nsa-global-method-security-proxy-target-class]] +* **proxy-target-class** +If true, class based proxying will be used instead of interface based proxying. + + +[[nsa-global-method-security-run-as-manager-ref]] +* **run-as-manager-ref** +A reference to an optional `RunAsManager` implementation which will be used by the configured `MethodSecurityInterceptor` + + +[[nsa-global-method-security-secured-annotations]] +* **secured-annotations** +Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. +Defaults to "disabled". + + +[[nsa-global-method-security-children]] +=== Child Elements of + + +* <> +* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] +* <> +* <> + + + +[[nsa-after-invocation-provider]] +== +This element can be used to decorate an `AfterInvocationProvider` for use by the security interceptor maintained by the `` namespace. +You can define zero or more of these within the `global-method-security` element, each with a `ref` attribute pointing to an `AfterInvocationProvider` bean instance within your application context. + + +[[nsa-after-invocation-provider-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-after-invocation-provider-attributes]] +=== Attributes + + +[[nsa-after-invocation-provider-ref]] +* **ref** +Defines a reference to a Spring bean that implements `AfterInvocationProvider`. + + +[[nsa-pre-post-annotation-handling]] +== +Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replaced entirely. +Only applies if these annotations are enabled. + + +[[nsa-pre-post-annotation-handling-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-pre-post-annotation-handling-children]] +=== Child Elements of + + +* <> +* <> +* <> + + + +[[nsa-invocation-attribute-factory]] +== +Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + + +[[nsa-invocation-attribute-factory-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-invocation-attribute-factory-attributes]] +=== Attributes + + +[[nsa-invocation-attribute-factory-ref]] +* **ref** +Defines a reference to a Spring bean Id. + + +[[nsa-post-invocation-advice]] +== +Customizes the `PostInvocationAdviceProvider` with the ref as the `PostInvocationAuthorizationAdvice` for the element. + + +[[nsa-post-invocation-advice-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-post-invocation-advice-attributes]] +=== Attributes + + +[[nsa-post-invocation-advice-ref]] +* **ref** +Defines a reference to a Spring bean Id. + + +[[nsa-pre-invocation-advice]] +== +Customizes the `PreInvocationAuthorizationAdviceVoter` with the ref as the `PreInvocationAuthorizationAdviceVoter` for the element. + + +[[nsa-pre-invocation-advice-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-pre-invocation-advice-attributes]] +=== Attributes + + +[[nsa-pre-invocation-advice-ref]] +* **ref** +Defines a reference to a Spring bean Id. + + +[[nsa-protect-pointcut]] +== Securing Methods using +`` +Rather than defining security attributes on an individual method or class basis using the `@Secured` annotation, you can define cross-cutting security constraints across whole sets of methods and interfaces in your service layer using the `` element. +You can find an example in the xref:servlet/authorization/method-security.adoc#ns-protect-pointcut[namespace introduction]. + + +[[nsa-protect-pointcut-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-protect-pointcut-attributes]] +=== Attributes + + +[[nsa-protect-pointcut-access]] +* **access** +Access configuration attributes list that applies to all methods matching the pointcut, e.g. +"ROLE_A,ROLE_B" + + +[[nsa-protect-pointcut-expression]] +* **expression** +An AspectJ expression, including the `execution` keyword. +For example, `execution(int com.foo.TargetObject.countLength(String))`. + + +[[nsa-intercept-methods]] +== +Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + + +[[nsa-intercept-methods-attributes]] +=== Attributes + + +[[nsa-intercept-methods-access-decision-manager-ref]] +* **access-decision-manager-ref** +Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + + +[[nsa-intercept-methods-children]] +=== Child Elements of + + +* <> + + + +[[nsa-method-security-metadata-source]] +== +Creates a MethodSecurityMetadataSource instance + + +[[nsa-method-security-metadata-source-attributes]] +=== Attributes + + +[[nsa-method-security-metadata-source-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-method-security-metadata-source-use-expressions]] +* **use-expressions** +Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. +Defaults to 'false'. +If enabled, each attribute should contain a single Boolean expression. +If the expression evaluates to 'true', access will be granted. + + +[[nsa-method-security-metadata-source-children]] +=== Child Elements of + + +* <> + + + +[[nsa-protect]] +== +Defines a protected method and the access control configuration attributes that apply to it. +We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + + +[[nsa-protect-parents]] +=== Parent Elements of + + +* <> +* <> + + + +[[nsa-protect-attributes]] +=== Attributes + + +[[nsa-protect-access]] +* **access** +Access configuration attributes list that applies to the method, e.g. +"ROLE_A,ROLE_B". + + +[[nsa-protect-method]] +* **method** +A method name diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc new file mode 100644 index 00000000000..ae645077370 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc @@ -0,0 +1,80 @@ +[[nsa-websocket-security]] += WebSocket Security + +Spring Security 4.0+ provides support for authorizing messages. +One concrete example of where this is useful is to provide authorization in WebSocket based applications. + +[[nsa-websocket-message-broker]] +== + +The websocket-message-broker element has two different modes. +If the <> is not specified, then it will do the following things: + +* Ensure that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver. +This allows the use of `@AuthenticationPrincipal` to resolve the principal of the current `Authentication` +* Ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel. +This populates the SecurityContextHolder with the user that is found in the Message +* Ensures that a ChannelSecurityInterceptor is registered with the clientInboundChannel. +This allows authorization rules to be specified for a message. +* Ensures that a CsrfChannelInterceptor is registered with the clientInboundChannel. +This ensures that only requests from the original domain are enabled. +* Ensures that a CsrfTokenHandshakeInterceptor is registered with WebSocketHttpRequestHandler, TransportHandlingSockJsService, or DefaultSockJsService. +This ensures that the expected CsrfToken from the HttpServletRequest is copied into the WebSocket Session attributes. + +If additional control is necessary, the id can be specified and a ChannelSecurityInterceptor will be assigned to the specified id. +All the wiring with Spring's messaging infrastructure can then be done manually. +This is more cumbersome, but provides greater control over the configuration. + + +[[nsa-websocket-message-broker-attributes]] +=== Attributes + +[[nsa-websocket-message-broker-id]] +* **id** A bean identifier, used for referring to the ChannelSecurityInterceptor bean elsewhere in the context. +If specified, Spring Security requires explicit configuration within Spring Messaging. +If not specified, Spring Security will automatically integrate with the messaging infrastructure as described in <> + +[[nsa-websocket-message-broker-same-origin-disabled]] +* **same-origin-disabled** Disables the requirement for CSRF token to be present in the Stomp headers (default false). +Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + +[[nsa-websocket-message-broker-authorization-manager-ref]] +* **authorization-manager-ref** Use this `AuthorizationManager` instance; when set, `use-authorization-manager` is ignored and assumed to be `true` + +[[nsa-websocket-message-broker-use-authorization-manager]] +* **use-authorization-manager** Uses legacy `SecurityMetadataSource` API instead of `AuthorizationManager` API (default false). + +[[nsa-websocket-message-broker-children]] +=== Child Elements of + + +* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] +* <> + +[[nsa-intercept-message]] +== + +Defines an authorization rule for a message. + + +[[nsa-intercept-message-parents]] +=== Parent Elements of + + +* <> + + +[[nsa-intercept-message-attributes]] +=== Attributes + +[[nsa-intercept-message-pattern]] +* **pattern** An ant based pattern that matches on the Message destination. +For example, "/**" matches any Message with a destination; "/admin/**" matches any Message that has a destination that starts with "/admin/**". + +[[nsa-intercept-message-type]] +* **type** The type of message to match on. +Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + +[[nsa-intercept-message-access]] +* **access** The expression used to secure the Message. +For example, "denyAll" will deny access to all of the matching Messages; "permitAll" will grant access to all of the matching Messages; "hasRole('ADMIN') requires the current user to have the role 'ROLE_ADMIN' for the matching Messages. diff --git a/docs/modules/ROOT/pages/servlet/architecture.adoc b/docs/modules/ROOT/pages/servlet/architecture.adoc index 40b62a1a3c8..f9ca15fdeb4 100644 --- a/docs/modules/ROOT/pages/servlet/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/architecture.adoc @@ -165,6 +165,7 @@ However, there are times that it is beneficial to know the ordering Below is a comprehensive list of Spring Security Filter ordering: +* xref:servlet/authentication/session-management.adoc#session-mgmt-force-session-creation[`ForceEagerSessionCreationFilter`] * ChannelProcessingFilter * WebAsyncManagerIntegrationFilter * SecurityContextPersistenceFilter diff --git a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc index ea61488c575..6b8ec271d53 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc @@ -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 <>. diff --git a/docs/modules/ROOT/pages/servlet/authentication/cas.adoc b/docs/modules/ROOT/pages/servlet/authentication/cas.adoc index 5b6dd7617ed..1917e156ae7 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/cas.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/cas.adoc @@ -139,7 +139,7 @@ The following beans should be configured to commence the CAS authentication proc ---- For CAS to operate, the `ExceptionTranslationFilter` must have its `authenticationEntryPoint` property set to the `CasAuthenticationEntryPoint` bean. -This can easily be done using xref:servlet/appendix/namespace.adoc#nsa-http-entry-point-ref[entry-point-ref] as is done in the example above. +This can easily be done using xref:servlet/appendix/namespace/http.adoc#nsa-http-entry-point-ref[entry-point-ref] as is done in the example above. The `CasAuthenticationEntryPoint` must refer to the `ServiceProperties` bean (discussed above), which provides the URL to the enterprise's CAS login server. This is where the user's browser will be redirected. diff --git a/docs/modules/ROOT/pages/servlet/authentication/index.adoc b/docs/modules/ROOT/pages/servlet/authentication/index.adoc index ceb08df6cbc..cc232247ca4 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/index.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/index.adoc @@ -14,7 +14,7 @@ These sections focus on specific ways you may want to authenticate and point bac // FIXME: brief description * xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd[Username and Password] - how to authenticate with a username/password -* xref:servlet/oauth2/oauth2-login.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub) +* xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub) * xref:servlet/saml2/index.adoc#servlet-saml2[SAML 2.0 Login] - SAML 2.0 Log In * xref:servlet/authentication/cas.adoc#servlet-cas[Central Authentication Server (CAS)] - Central Authentication Server (CAS) Support * xref:servlet/authentication/rememberme.adoc#servlet-rememberme[Remember Me] - how to remember a user past session expiration diff --git a/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc b/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc index 9e241932be3..4b8e960c07e 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc @@ -166,5 +166,5 @@ This means that the `Subject` can be accessed using: Subject subject = Subject.getSubject(AccessController.getContext()); ---- -This integration can easily be configured using the xref:servlet/appendix/namespace.adoc#nsa-http-jaas-api-provision[jaas-api-provision] attribute. +This integration can easily be configured using the xref:servlet/appendix/namespace/http.adoc#nsa-http-jaas-api-provision[jaas-api-provision] attribute. This feature is useful when integrating with legacy or external API's that rely on the JAAS Subject being populated. diff --git a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc index 909f1c3d755..23a55e11106 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc @@ -4,7 +4,7 @@ [[logout-java-configuration]] == Logout Java/Kotlin Configuration -When using the `{security-api-url}org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html[WebSecurityConfigurerAdapter]`, logout capabilities are automatically applied. +When injecting the `{security-api-url}org/springframework/security/config/annotation/web/builders/HttpSecurity.html[HttpSecurity]` bean, logout capabilities are automatically applied. The default is that accessing the URL `/logout` will log the user out by: - Invalidating the HTTP Session @@ -19,7 +19,7 @@ Similar to configuring login capabilities, however, you also have various option .Java [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) throws Exception { +public SecurityFilterChain filterChain(HttpSecurity http) { http .logout(logout -> logout // <1> .logoutUrl("/my/logout") // <2> @@ -36,7 +36,7 @@ protected void configure(HttpSecurity http) throws Exception { .Kotlin [source,kotlin,role="secondary"] ----- -override fun configure(http: HttpSecurity) { +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { logout { logoutUrl = "/my/logout" // <1> @@ -47,12 +47,12 @@ override fun configure(http: HttpSecurity) { deleteCookies(cookieNamesToClear) // <6> } } + // ... } ----- ==== <1> Provides logout support. -This is automatically applied when using `WebSecurityConfigurerAdapter`. <2> The URL that triggers log out to occur (default is `/logout`). If CSRF protection is enabled (default), then the request must also be a POST. For more information, please consult the {security-api-url}org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutUrl-java.lang.String-[Javadoc]. @@ -74,7 +74,7 @@ This is a shortcut for adding a `CookieClearingLogoutHandler` explicitly. [NOTE] ==== Logouts can of course also be configured using the XML Namespace notation. -Please see the documentation for the xref:servlet/appendix/namespace.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section for further details. +Please see the documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section for further details. ==== Generally, in order to customize logout functionality, you can add @@ -145,4 +145,4 @@ If not configured a status code 200 will be returned by default. - xref:servlet/authentication/rememberme.adoc#remember-me-impls[Remember-Me Interfaces and Implementations] - xref:servlet/exploits/csrf.adoc#servlet-considerations-csrf-logout[ Logging Out] in section CSRF Caveats - Section xref:servlet/authentication/cas.adoc#cas-singlelogout[ Single Logout] (CAS protocol) -- Documentation for the xref:servlet/appendix/namespace.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section +- Documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc index f83ead53aca..0a91b86b5c6 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc @@ -62,10 +62,12 @@ A minimal, explicit configuration can be found below: [source,java,role="primary"] .Java ---- -protected void configure(HttpSecurity http) { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .httpBasic(withDefaults()); + return http.build(); } ---- @@ -81,11 +83,13 @@ protected void configure(HttpSecurity http) { [source,kotlin,role="secondary"] .Kotlin ---- -fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... httpBasic { } } + return http.build() } ---- ==== diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc index 913f175e360..2e1b240c0cf 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc @@ -57,11 +57,13 @@ DigestAuthenticationFilter digestAuthenticationFilter() { result.setAuthenticationEntryPoint(entryPoint()); } -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint())) .addFilterBefore(digestFilter()); + return http.build(); } ---- diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc index 941cbb037f3..c01afe9fdcc 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc @@ -71,10 +71,10 @@ A minimal, explicit Java configuration can be found below: .Java [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) { +public SecurityFilterChain filterChain(HttpSecurity http) { http - // ... .formLogin(withDefaults()); + // ... } ---- @@ -90,11 +90,11 @@ protected void configure(HttpSecurity http) { .Kotlin [source,kotlin,role="secondary"] ---- -fun configure(http: HttpSecurity) { +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { - // ... formLogin { } } + // ... } ---- ==== @@ -110,13 +110,13 @@ The configuration below demonstrates how to provide a custom log in form. .Java [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) throws Exception { +public SecurityFilterChain filterChain(HttpSecurity http) { http - // ... .formLogin(form -> form .loginPage("/login") .permitAll() ); + // ... } ---- @@ -133,14 +133,14 @@ protected void configure(HttpSecurity http) throws Exception { .Kotlin [source,kotlin,role="secondary"] ---- -fun configure(http: HttpSecurity) { +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { - // ... formLogin { loginPage = "/login" permitAll() } } + // ... } ---- ==== diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc index d4462eaaf8d..c5ca66a613d 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc @@ -116,7 +116,7 @@ In our example, we will setup an https://docs.spring.io/spring-framework/docs/cu DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(H2) - .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl") + .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION) .build(); } ---- @@ -136,7 +136,7 @@ DataSource dataSource() { fun dataSource(): DataSource { return EmbeddedDatabaseBuilder() .setType(H2) - .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl") + .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION) .build() } ---- diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/ldap.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/ldap.adoc index aa4ad900ff8..127c71fb999 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/ldap.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/ldap.adoc @@ -36,7 +36,7 @@ If you are unfamiliar with how to do this, you can refer to the https://docs.ora == Setting up an Embedded LDAP Server The first thing you will need to do is to ensure that you have an LDAP Server to point your configuration to. -For simplicity, it often best to start with an embedded LDAP Server. +For simplicity, it is often best to start with an embedded LDAP Server. Spring Security supports using either: * <> @@ -118,7 +118,8 @@ depenendencies { ---- ==== -You can then configure the Embedded LDAP Server +You can then configure the Embedded LDAP Server using an `EmbeddedLdapServerContextSourceFactoryBean`. +This will instruct Spring Security to start an in-memory LDAP server. .Embedded LDAP Server Configuration ==== @@ -126,6 +127,30 @@ You can then configure the Embedded LDAP Server [source,java,role="primary"] ---- @Bean +public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun contextSourceFactoryBean(): EmbeddedLdapServerContextSourceFactoryBean { + return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer() +} +---- +==== + +Alternatively, you can manually configure the Embedded LDAP Server. +If you choose this approach, you will be responsible for managing the lifecycle of the Embedded LDAP Server. + +.Explicit Embedded LDAP Server Configuration +==== +.Java +[source,java,role="primary"] +---- +@Bean UnboundIdContainer ldapContainer() { return new UnboundIdContainer("dc=springframework,dc=org", "classpath:users.ldif"); @@ -228,6 +253,35 @@ fun ldapContainer(): ApacheDSContainer { Once you have an LDAP Server to point your configuration to, you need configure Spring Security to point to an LDAP server that should be used to authenticate users. This is done by creating an LDAP `ContextSource`, which is the equivalent of a JDBC `DataSource`. +If you have already configured an `EmbeddedLdapServerContextSourceFactoryBean`, Spring Security will create an LDAP `ContextSource` that points to the embedded LDAP server. + +.LDAP Context Source with Embedded LDAP Server +==== +.Java +[source,java,role="primary"] +---- +@Bean +public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean = + EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer(); + contextSourceFactoryBean.setPort(0); + return contextSourceFactoryBean; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun contextSourceFactoryBean(): EmbeddedLdapServerContextSourceFactoryBean { + val contextSourceFactoryBean = EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer() + contextSourceFactoryBean.setPort(0) + return contextSourceFactoryBean +} +---- +==== + +Alternatively, you can explicitly configure the LDAP `ContextSource` to connect to the supplied LDAP server. .LDAP Context Source ==== @@ -287,15 +341,10 @@ An example of bind authentication configuration can be found below. [source,java,role="primary",attrs="-attributes"] ---- @Bean -BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) { - BindAuthenticator authenticator = new BindAuthenticator(contextSource); - authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); - return authenticator; -} - -@Bean -LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { - return new LdapAuthenticationProvider(authenticator); +AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); } ---- @@ -310,15 +359,10 @@ LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticato [source,kotlin,role="secondary",attrs="-attributes"] ---- @Bean -fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator { - val authenticator = BindAuthenticator(contextSource) - authenticator.setUserDnPatterns(arrayOf("uid={0},ou=people")) - return authenticator -} - -@Bean -fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { - return LdapAuthenticationProvider(authenticator) +fun authenticationManager(contextSource: BaseLdapPathContextSource): AuthenticationManager { + val factory = LdapBindAuthenticationManagerFactory(contextSource) + factory.setUserDnPatterns("uid={0},ou=people") + return factory.createAuthenticationManager() } ---- ==== @@ -333,19 +377,11 @@ If instead you wished to configure an LDAP search filter to locate the user, you [source,java,role="primary",attrs="-attributes"] ---- @Bean -BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) { - String searchBase = "ou=people"; - String filter = "(uid={0})"; - FilterBasedLdapUserSearch search = - new FilterBasedLdapUserSearch(searchBase, filter, contextSource); - BindAuthenticator authenticator = new BindAuthenticator(contextSource); - authenticator.setUserSearch(search); - return authenticator; -} - -@Bean -LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { - return new LdapAuthenticationProvider(authenticator); +AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserSearchFilter("(uid={0})"); + factory.setUserSearchBase("ou=people"); + return factory.createAuthenticationManager(); } ---- @@ -361,18 +397,11 @@ LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticato [source,kotlin,role="secondary",attrs="-attributes"] ---- @Bean -fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator { - val searchBase = "ou=people" - val filter = "(uid={0})" - val search = FilterBasedLdapUserSearch(searchBase, filter, contextSource) - val authenticator = BindAuthenticator(contextSource) - authenticator.setUserSearch(search) - return authenticator -} - -@Bean -fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { - return LdapAuthenticationProvider(authenticator) +fun authenticationManager(contextSource: BaseLdapPathContextSource): AuthenticationManager { + val factory = LdapBindAuthenticationManagerFactory(contextSource) + factory.setUserSearchFilter("(uid={0})") + factory.setUserSearchBase("ou=people") + return factory.createAuthenticationManager() } ---- ==== @@ -394,13 +423,11 @@ An LDAP compare cannot be done when the password is properly hashed with a rando [source,java,role="primary"] ---- @Bean -PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) { - return new PasswordComparisonAuthenticator(contextSource); -} - -@Bean -LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { - return new LdapAuthenticationProvider(authenticator); +AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); } ---- @@ -417,13 +444,12 @@ LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticato [source,kotlin,role="secondary"] ---- @Bean -fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator { - return PasswordComparisonAuthenticator(contextSource) -} - -@Bean -fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { - return LdapAuthenticationProvider(authenticator) +fun authenticationManager(contextSource: BaseLdapPathContextSource?): AuthenticationManager? { + val factory = LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance() + ) + factory.setUserDnPatterns("uid={0},ou=people") + return factory.createAuthenticationManager() } ---- ==== @@ -436,17 +462,12 @@ A more advanced configuration with some customizations can be found below. [source,java,role="primary"] ---- @Bean -PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) { - PasswordComparisonAuthenticator authenticator = - new PasswordComparisonAuthenticator(contextSource); - authenticator.setPasswordAttributeName("pwd"); // <1> - authenticator.setPasswordEncoder(new BCryptPasswordEncoder()); // <2> - return authenticator; -} - -@Bean -LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { - return new LdapAuthenticationProvider(authenticator); +AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, new BCryptPasswordEncoder()); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setPasswordAttribute("pwd"); // <1> + return factory.createAuthenticationManager(); } ---- @@ -467,23 +488,18 @@ LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticato [source,kotlin,role="secondary"] ---- @Bean -fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator { - val authenticator = PasswordComparisonAuthenticator(contextSource) - authenticator.setPasswordAttributeName("pwd") // <1> - authenticator.setPasswordEncoder(BCryptPasswordEncoder()) // <2> - return authenticator -} - -@Bean -fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { - return LdapAuthenticationProvider(authenticator) +fun authenticationManager(contextSource: BaseLdapPathContextSource): AuthenticationManager { + val factory = LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, BCryptPasswordEncoder() + ) + factory.setUserDnPatterns("uid={0},ou=people") + factory.setPasswordAttribute("pwd") // <1> + return factory.createAuthenticationManager() } ---- ==== <1> Specify the password attribute as `pwd` -<2> Use `BCryptPasswordEncoder` - == LdapAuthoritiesPopulator @@ -504,8 +520,11 @@ LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) { } @Bean -LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authorities) { - return new LdapAuthenticationProvider(authenticator, authorities); +AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource, LdapAuthoritiesPopulator authorities) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setLdapAuthoritiesPopulator(authorities); + return factory.createAuthenticationManager(); } ---- @@ -529,8 +548,13 @@ fun authorities(contextSource: BaseLdapPathContextSource): LdapAuthoritiesPopula } @Bean -fun authenticationProvider(authenticator: LdapAuthenticator, authorities: LdapAuthoritiesPopulator): LdapAuthenticationProvider { - return LdapAuthenticationProvider(authenticator, authorities) +fun authenticationManager( + contextSource: BaseLdapPathContextSource, + authorities: LdapAuthoritiesPopulator): AuthenticationManager { + val factory = LdapBindAuthenticationManagerFactory(contextSource) + factory.setUserDnPatterns("uid={0},ou=people") + factory.setLdapAuthoritiesPopulator(authorities) + return factory.createAuthenticationManager() } ---- ==== diff --git a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc new file mode 100644 index 00000000000..6d88332634b --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc @@ -0,0 +1,172 @@ +[[persistant]] += Persisting Authentication +:figures: servlet/authentication + +The first time a user requests a protected resource, they are xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[prompted for credentials]. +One of the most common ways to prompt for credentials is to redirect the user to a xref:servlet/authentication/passwords/form.adoc[log in page]. +A summarized HTTP exchange for an unauthenticated user requesting a protected resource might look like this: + +.Unauthenticated User Requests Protected Resource +==== +[source,http] +---- +GET / HTTP/1.1 +Host: example.com +Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b +---- + +[source,http] +---- +HTTP/1.1 302 Found +Location: /login +---- +==== + +The user submits their username and password. + +.Username and Password Submitted +==== +[source,http] +---- +POST /login HTTP/1.1 +Host: example.com +Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b + +username=user&password=password&_csrf=35942e65-a172-4cd4-a1d4-d16a51147b3e +---- +==== + +Upon authenticating the user, the user is associated to a new session id to prevent xref:servlet/authentication/session-management.adoc#ns-session-fixation[session fixation attacks]. + +.Authenticated User is Associated to New Session +==== +[source,http] +---- +HTTP/1.1 302 Found +Location: / +Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax +---- +==== + +Subsequent requests include the session cookie which is used to authenticate the user for the remainder of the session. + +.Authenticated Session Provided as Credentials +==== +[source,http] +---- +GET / HTTP/1.1 +Host: example.com +Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8 +---- +==== + + +[[securitycontextrepository]] +== SecurityContextRepository + +// FIXME: api documentation +In Spring Security the association of the user to future requests is made using {security-api-url}org/springframework/security/web/context/SecurityContextRepository.html[`SecurityContextRepository`]. + +[[httpsecuritycontextrepository]] +=== HttpSecurityContextRepository + +The default implementation of `SecurityContextRepository` is {security-api-url}org/springframework/security/web/context/HttpSessionSecurityContextRepository.html[`HttpSessionSecurityContextRepository`] which associates the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[`SecurityContext`] to the `HttpSession`. +Users can replace `HttpSessionSecurityContextRepository` with another implementation of `SecurityContextRepository` if they wish to associate the user with subsequent requests in another way or not at all. + +[[nullsecuritycontextrepository]] +=== NullSecurityContextRepository + +If it is not desirable to associate the `SecurityContext` to an `HttpSession` (i.e. when authenticating with OAuth) the {security-api-url}org/springframework/security/web/context/NullSecurityContextRepository.html[`NullSecurityContextRepository`] is an implementation of `SecurityContextRepository` that does nothing. + +[[requestattributesecuritycontextrepository]] +=== RequestAttributeSecurityContextRepository + +The {security-api-url}org/springframework/security/web/context/RequestAttributeSecurityContextRepository.html[`RequestAttributeSecurityContextRepository`] saves the `SecurityContext` as a request attribute to make sure the `SecurityContext` is avaible for a single request that occurs across dispatch types that may clear out the `SecurityContext`. + +For example, assume that a client makes a request, is authenticated, and then an error occurs. +Depending on the servlet container implementation, the error means that any `SecurityContext` that was established is cleared out and then the error dispatch is made. +When the error dispatch is made, there is no `SecurityContext` established. +This means that the error page cannot use the `SecurityContext` for authorization or displaying the current user unless the `SecurityContext` is persisted somehow. + +.Use RequestAttributeSecurityContextRepository +==== +.Java +[source,java,role="primary"] +---- +public SecurityFilterChain filterChain(HttpSecurity http) { + http + // ... + .securityContext((securityContext) -> securityContext + .securityContextRepository(new RequestAttributeSecurityContextRepository()) + ); + return http.build(); +} +---- + +.XML +[source,xml,role="secondary"] +---- + + + + +---- +==== + + +[[securitycontextpersistencefilter]] +== SecurityContextPersistenceFilter + +The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`]. + +image::{figures}/securitycontextpersistencefilter.png[] + +<1> Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`. +<2> Next, the application is ran. +<3> Finally, if the `SecurityContext` has changed, we save the `SecurityContext` using the `SecurityContextPersistenceRepository`. +This means that when using `SecurityContextPersistenceFilter`, just setting the `SecurityContextHolder` will ensure that the `SecurityContext` is persisted using `SecurityContextRepository`. + +In some cases a response is committed and written to the client before the `SecurityContextPersisteneFilter` method completes. +For example, if a redirect is sent to the client the response is immediately written back to the client. +This means that establishing an `HttpSession` would not be possible in step 3 because the session id could not be included in the already written response. +Another situation that can happen is that if a client authenticates successfully, the response is committed before `SecurityContextPersistenceFilter` completes, and the client makes a second request before the `SecurityContextPersistenceFilter` completes the wrong authentication could be present in the second request. + +To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `HttpServletRequest` and the `HttpServletResponse` to detect if the `SecurityContext` has changed and if so save the `SecurityContext` just before the response is committed. + +[[securitycontextholderfilter]] +== SecurityContextHolderFilter + +The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`]. + +image::{figures}/securitycontextholderfilter.png[] + +<1> Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`. +<2> Next, the application is ran. + +Unlike, xref:servlet/authentication/persistence.adoc#securitycontextpersistencefilter[`SecurityContextPersisteneFilter`], `SecurityContextHolderFilter` only loads the `SecurityContext` it does not save the `SecurityContext`. +This means that when using `SecurityContextHolderFilter`, it is required that the `SecurityContext` is explicitly saved. + +.Explicit Saving of SecurityContext +==== +.Java +[source,java,role="primary"] +---- +public SecurityFilterChain filterChain(HttpSecurity http) { + http + // ... + .securityContext((securityContext) -> securityContext + .requireExplicitSave(true) + ); + return http.build(); +} +---- + +.XML +[source,xml,role="secondary"] +---- + + + +---- +==== \ No newline at end of file diff --git a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc index 6024d34a1bd..f3459bf1508 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc @@ -3,6 +3,35 @@ HTTP session related functionality is handled by a combination of the `SessionManagementFilter` and the `SessionAuthenticationStrategy` interface, which the filter delegates to. Typical usage includes session-fixation protection attack prevention, detection of session timeouts and restrictions on how many sessions an authenticated user may have open concurrently. +[[session-mgmt-force-session-creation]] +== Force Eager Session Creation + +At times it can be valuable to eagerly create sessions. +This can be done by using the {security-api-url}org/springframework/security/web/session/ForceEagerSessionCreationFilter.html[`ForceEagerSessionCreationFilter`] which can be configured using: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { + http + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) + ); + return http.build(); +} +---- + +.XML +[source,xml,role="secondary"] +---- + + + +---- +==== + == Detecting Timeouts You can configure Spring Security to detect the submission of an invalid session ID and redirect the user to an appropriate URL. This is achieved through the `session-management` element: @@ -11,12 +40,13 @@ This is achieved through the `session-management` element: .Java [source,java,role="primary"] ---- -@Override -protected void configure(HttpSecurity http) throws Exception{ +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { http .sessionManagement(session -> session .invalidSessionUrl("/invalidSession.htm") ); + return http.build(); } ---- @@ -38,12 +68,13 @@ You may be able to explicitly delete the JSESSIONID cookie on logging out, for e .Java [source,java,role="primary"] ---- -@Override -protected void configure(HttpSecurity http) throws Exception{ +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { http .logout(logout -> logout .deleteCookies("JSESSIONID") ); + return http.build(); } ---- @@ -105,12 +136,13 @@ Then add the following lines to your application context: .Java [source,java,role="primary"] ---- -@Override -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { http .sessionManagement(session -> session .maximumSessions(1) ); + return http.build(); } ---- @@ -134,13 +166,14 @@ Often you would prefer to prevent a second login, in which case you can use .Java [source,java,role="primary"] ---- -@Override -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { http .sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(true) ); + return http.build(); } ---- diff --git a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc index dd4ad3ee19f..bebbccaaa8f 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc @@ -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 <>. + +Applications that customize an `AccessDecisionManager` or `AccessDecisionVoter` are encouraged to <>. + +``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, Object secureObject); + +default AuthorizationDecision verify(Supplier 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`. + +<> 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 <>. + +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, Object object) { + try { + Collection 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, Object object) { + Collection 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, Object object) { + Collection 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 RoleHierarchyVoter(hierarchy); +} +---- + +.Xml +[source,java,role="secondary"] +---- + + + + + + + + ROLE_ADMIN > ROLE_STAFF + ROLE_STAFF > ROLE_USER + ROLE_USER > ROLE_GUEST + + + +---- +==== + +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. - -<> 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 <>, `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] ----- - - - - - - - - ROLE_ADMIN > ROLE_STAFF - ROLE_STAFF > ROLE_USER - ROLE_USER > ROLE_GUEST - - - ----- - -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. diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc new file mode 100644 index 00000000000..8b994db9a67 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -0,0 +1,207 @@ +[[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` 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(new WebExpressionAuthorizationManager("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 access) + throws AuthenticationException { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().access(access) + ) + // ... + + return http.build(); +} + +@Bean +AuthorizationManager 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 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(); +} +---- +==== + +By default, the `AuthorizationFilter` does not apply to `DispatcherType.ERROR` and `DispatcherType.ASYNC`. +We can configure Spring Security to apply the authorization rules to all dispatcher types by using the `shouldFilterAllDispatcherTypes` method: + +.Set shouldFilterAllDispatcherTypes to true +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .shouldFilterAllDispatcherTypes(true) + .anyRequest.authenticated() + ) + // ... + + return http.build(); +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +open fun web(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + shouldFilterAllDispatcherTypes = true + authorize(anyRequest, authenticated) + } + } + return http.build() +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc index bbf771a73b7..3009ba4f365 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc @@ -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,17 +28,20 @@ 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 [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .authorizeRequests(authorize -> authorize .anyRequest().authenticated() ); + return http.build(); } ---- @@ -50,13 +57,15 @@ protected void configure(HttpSecurity http) throws Exception { .Kotlin [source,kotlin,role="secondary"] ---- -fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... authorizeRequests { authorize(anyRequest, authenticated) } } + return http.build() } ---- ==== @@ -68,7 +77,8 @@ We can configure Spring Security to have different rules by adding more rules in .Java [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .authorizeRequests(authorize -> authorize // <1> @@ -77,6 +87,7 @@ protected void configure(HttpSecurity http) throws Exception { .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4> .anyRequest().denyAll() // <5> ); + return http.build(); } ---- @@ -99,7 +110,8 @@ protected void configure(HttpSecurity http) throws Exception { .Kotlin [source,kotlin,role="secondary"] ---- -fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { // <1> authorize("/resources/**", permitAll) // <2> @@ -111,6 +123,7 @@ fun configure(http: HttpSecurity) { authorize(anyRequest, denyAll) // <5> } } + return http.build() } ---- ==== diff --git a/docs/modules/ROOT/pages/servlet/authorization/events.adoc b/docs/modules/ROOT/pages/servlet/authorization/events.adoc new file mode 100644 index 00000000000..d85611a0d39 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authorization/events.adoc @@ -0,0 +1,160 @@ +[[servlet-events]] += Authorization Events + +For each authorization that is denied, an `AuthorizationDeniedEvent` is fired. +Also, it's possible to fire and `AuthorizationGrantedEvent` for authorizations that are granted. + +To listen for these events, you must first publish an `AuthorizationEventPublisher`. + +Spring Security's `SpringAuthorizationEventPublisher` will probably do fine. +It comes publishes authorization events using Spring's `ApplicationEventPublisher`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public AuthorizationEventPublisher authorizationEventPublisher + (ApplicationEventPublisher applicationEventPublisher) { + return new SpringAuthorizationEventPublisher(applicationEventPublisher); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizationEventPublisher + (applicationEventPublisher: ApplicationEventPublisher?): AuthorizationEventPublisher { + return SpringAuthorizationEventPublisher(applicationEventPublisher) +} +---- +==== + +Then, you can use Spring's `@EventListener` support: + +==== +.Java +[source,java,role="primary"] +---- +@Component +public class AuthenticationEvents { + + @EventListener + public void onFailure(AuthorizationDeniedEvent failure) { + // ... + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Component +class AuthenticationEvents { + + @EventListener + fun onFailure(failure: AuthorizationDeniedEvent?) { + // ... + } +} +---- +==== + +[[authorization-granted-events]] +== Authorization Granted Events + +Because ``AuthorizationGrantedEvent``s have the potential to be quite noisy, they are not published by default. + +In fact, publishing these events will likely require some business logic on your part to ensure that your application is not inundated with noisy authorization events. + +You can create your own event publisher that filters success events. +For example, the following publisher only publishes authorization grants where `ROLE_ADMIN` was required: + +==== +.Java +[source,java,role="primary"] +---- +@Component +public class MyAuthorizationEventPublisher implements AuthorizationEventPublisher { + private final ApplicationEventPublisher publisher; + private final AuthorizationEventPublisher delegate; + + public MyAuthorizationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + this.delegate = new SpringAuthorizationEventPublisher(publisher); + } + + @Override + public void publishAuthorizationEvent(Supplier authentication, + T object, AuthorizationDecision decision) { + if (decision == null) { + return; + } + if (!decision.isGranted()) { + this.delegate.publishAuthorizationEvent(authentication, object, decision); + return; + } + if (shouldThisEventBePublished(decision)) { + AuthorizationGrantedEvent granted = new AuthorizationGrantedEvent( + authentication, object, decision); + this.publisher.publishEvent(granted); + } + } + + private boolean shouldThisEventBePublished(AuthorizationDecision decision) { + if (!(decision instanceof AuthorityAuthorizationDecision)) { + return false; + } + Collection authorities = ((AuthorityAuthorizationDecision) decision).getAuthorities(); + for (GrantedAuthority authority : authorities) { + if ("ROLE_ADMIN".equals(authority.getAuthority())) { + return true; + } + } + return false; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Component +class MyAuthorizationEventPublisher(val publisher: ApplicationEventPublisher, + val delegate: SpringAuthorizationEventPublisher = SpringAuthorizationEventPublisher(publisher)): + AuthorizationEventPublisher { + + override fun publishAuthorizationEvent( + authentication: Supplier?, + `object`: T, + decision: AuthorizationDecision? + ) { + if (decision == null) { + return + } + if (!decision.isGranted) { + this.delegate.publishAuthorizationEvent(authentication, `object`, decision) + return + } + if (shouldThisEventBePublished(decision)) { + val granted = AuthorizationGrantedEvent(authentication, `object`, decision) + this.publisher.publishEvent(granted) + } + } + + private fun shouldThisEventBePublished(decision: AuthorizationDecision): Boolean { + if (decision !is AuthorityAuthorizationDecision) { + return false + } + val authorities = decision.authorities + for (authority in authorities) { + if ("ROLE_ADMIN" == authority.authority) { + return true + } + } + return false + } +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc index f5b916d6360..218b6dec37b 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc @@ -144,7 +144,7 @@ You could refer to the method using: [source,java,role="primary"] ---- http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") ... ) @@ -210,7 +210,7 @@ You could refer to the method using: [source,java,role="primary",attrs="-attributes"] ---- http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)") ... ); diff --git a/docs/modules/ROOT/pages/servlet/configuration/java.adoc b/docs/modules/ROOT/pages/servlet/configuration/java.adoc index 985a01c5169..8c440d5dde8 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/java.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/java.adoc @@ -135,18 +135,20 @@ public class MvcWebApplicationInitializer extends Thus far our <> only contains information about how to authenticate our users. How does Spring Security know that we want to require all users to be authenticated? How does Spring Security know we want to support form based authentication? -Actually, there is a configuration class that is being invoked behind the scenes called `WebSecurityConfigurerAdapter`. -It has a method called `configure` with the following default implementation: +Actually, there is a bean that is being invoked behind the scenes called `SecurityFilterChain`. +It is configured with the following default implementation: [source,java] ---- -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests(authorize -> authorize .anyRequest().authenticated() ) .formLogin(withDefaults()) .httpBasic(withDefaults()); + return http.build(); } ---- @@ -169,8 +171,8 @@ You will notice that this configuration is quite similar the XML Namespace confi == Multiple HttpSecurity -We can configure multiple HttpSecurity instances just as we can have multiple `` blocks. -The key is to extend the `WebSecurityConfigurerAdapter` multiple times. +We can configure multiple `HttpSecurity` instances just as we can have multiple `` blocks. +The key is to register multiple `SecurityFilterChain` ``@Bean``s. For example, the following is an example of having a different configuration for URL's that start with `/api/`. [source,java] @@ -187,40 +189,36 @@ public class MultiHttpSecurityConfig { return manager; } - @Configuration + @Bean @Order(1) <2> - public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) throws Exception { - http - .antMatcher("/api/**") <3> - .authorizeRequests(authorize -> authorize - .anyRequest().hasRole("ADMIN") - ) - .httpBasic(withDefaults()); - } + public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { + http + .antMatcher("/api/**") <3> + .authorizeHttpRequests(authorize -> authorize + .anyRequest().hasRole("ADMIN") + ) + .httpBasic(withDefaults()); + return http.build(); } - @Configuration <4> - public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .formLogin(withDefaults()); - } + @Bean <4> + public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .formLogin(withDefaults()); + return http.build(); } } ---- <1> Configure Authentication as normal -<2> Create an instance of `WebSecurityConfigurerAdapter` that contains `@Order` to specify which `WebSecurityConfigurerAdapter` should be considered first. +<2> Register an instance of `SecurityFilterChain` that contains `@Order` to specify which `SecurityFilterChain` should be considered first. <3> The `http.antMatcher` states that this `HttpSecurity` will only be applicable to URLs that start with `/api/` -<4> Create another instance of `WebSecurityConfigurerAdapter`. +<4> Register another instance of `SecurityFilterChain`. If the URL does not start with `/api/` this configuration will be used. -This configuration is considered after `ApiWebSecurityConfigurationAdapter` since it has an `@Order` value after `1` (no `@Order` defaults to last). +This configuration is considered after `apiFilterChain` since it has an `@Order` value after `1` (no `@Order` defaults to last). [[jc-custom-dsls]] == Custom DSLs @@ -268,14 +266,15 @@ The custom DSL can then be used like this: [source,java] ---- @EnableWebSecurity -public class Config extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { +public class Config { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .apply(customDsl()) .flag(true) .and() ...; + return http.build(); } } ---- @@ -286,7 +285,7 @@ The code is invoked in the following order: * Code in `MyCustomDsl`s init method is invoked * Code in `MyCustomDsl`s configure method is invoked -If you want, you can have `WebSecurityConfigurerAdapter` add `MyCustomDsl` by default by using `SpringFactories`. +If you want, you can add `MyCustomDsl` to `HttpSecurity` by default by using `SpringFactories`. For example, you would create a resource on the classpath named `META-INF/spring.factories` with the following contents: .META-INF/spring.factories @@ -299,12 +298,13 @@ Users wishing to disable the default can do so explicitly. [source,java] ---- @EnableWebSecurity -public class Config extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { +public class Config { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .apply(customDsl()).disable() ...; + return http.build(); } } ---- @@ -322,8 +322,8 @@ For example, if you wanted to configure the `filterSecurityPublishAuthorizationS [source,java] ---- -@Override -protected void configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests(authorize -> authorize .anyRequest().authenticated() @@ -335,5 +335,6 @@ protected void configure(HttpSecurity http) throws Exception { } }) ); + return http.build(); } ---- diff --git a/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc b/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc index 767ab7ed805..06861efa292 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc @@ -11,12 +11,13 @@ NOTE: Spring Security provides https://github.com/spring-projects/spring-securit How does Spring Security know that we want to require all users to be authenticated? How does Spring Security know we want to support form based authentication? -There is a configuration class that is being invoked behind the scenes called `WebSecurityConfigurerAdapter`. -It has a method called `configure` with the following default implementation: +Actually, there is a bean that is being invoked behind the scenes called `SecurityFilterChain`. +It is configured with the following default implementation: [source,kotlin] ---- -fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -24,6 +25,7 @@ fun configure(http: HttpSecurity) { formLogin { } httpBasic { } } + return http.build() } ---- @@ -47,7 +49,7 @@ You will notice that this configuration is quite similar the XML Namespace confi == Multiple HttpSecurity We can configure multiple HttpSecurity instances just as we can have multiple `` blocks. -The key is to extend the `WebSecurityConfigurerAdapter` multiple times. +The key is to register multiple `SecurityFilterChain` ``@Bean``s. For example, the following is an example of having a different configuration for URL's that start with `/api/`. [source,kotlin] @@ -63,37 +65,35 @@ class MultiHttpSecurityConfig { return manager } - @Configuration @Order(1) <2> - class ApiWebSecurityConfigurationAdapter: WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - securityMatcher("/api/**") <3> - authorizeRequests { - authorize(anyRequest, hasRole("ADMIN")) - } - httpBasic { } + @Bean + open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher("/api/**") <3> + authorizeRequests { + authorize(anyRequest, hasRole("ADMIN")) } + httpBasic { } } + return http.build() } - @Configuration <4> - class FormLoginWebSecurityConfigurerAdapter: WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - formLogin { } + @Bean <4> + open fun formLoginFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize(anyRequest, authenticated) } + formLogin { } } + return http.build() } } ---- <1> Configure Authentication as normal -<2> Create an instance of `WebSecurityConfigurerAdapter` that contains `@Order` to specify which `WebSecurityConfigurerAdapter` should be considered first. +<2> Expose an instance of `SecurityFilterChain` that contains `@Order` to specify which `SecurityFilterChain` should be considered first. <3> The `http.antMatcher` states that this `HttpSecurity` will only be applicable to URLs that start with `/api/` -<4> Create another instance of `WebSecurityConfigurerAdapter`. +<4> Expose another instance of `SecurityFilterChain`. If the URL does not start with `/api/` this configuration will be used. -This configuration is considered after `ApiWebSecurityConfigurationAdapter` since it has an `@Order` value after `1` (no `@Order` defaults to last). +This configuration is considered after `apiFilterChain` since it has an `@Order` value after `1` (no `@Order` defaults to last). diff --git a/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc b/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc index 0168114c1b4..3f834987379 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc @@ -192,7 +192,7 @@ Common problems like incorrect filter ordering are no longer an issue as the fil The `` element creates a `DaoAuthenticationProvider` bean and the `` element creates an `InMemoryDaoImpl`. All `authentication-provider` elements must be children of the `` element, which creates a `ProviderManager` and registers the authentication providers with it. -You can find more detailed information on the beans that are created in the xref:servlet/appendix/namespace.adoc#appendix-namespace[namespace appendix]. +You can find more detailed information on the beans that are created in the xref:servlet/appendix/namespace/index.adoc#appendix-namespace[namespace appendix]. It's worth cross-checking this if you want to start understanding what the important classes in the framework are and how they are used, particularly if you want to customise things later. **** @@ -253,6 +253,14 @@ The filters are listed in the order in which they occur in the filter chain. |=== | Alias | Filter Class | Namespace Element or Attribute +| DISABLE_ENCODE_URL_FILTER +| `DisableEncodeUrlFilter` +| `http@disable-url-rewriting` + +| FORCE_EAGER_SESSION_FILTER +| `ForceEagerSessionCreationFilter` +| `http@create-session="ALWAYS"` + | CHANNEL_FILTER | `ChannelProcessingFilter` | `http/intercept-url@requires-channel` diff --git a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc index fc2d4907353..39447e3ea45 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc @@ -65,15 +65,15 @@ You can configure `CookieCsrfTokenRepository` in Java Configuration using: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ); + return http.build(); } } ---- @@ -82,14 +82,16 @@ public class WebSecurityConfig extends [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { csrf { csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() } } + return http.build() } } ---- @@ -130,13 +132,13 @@ The Java configuration below will disable CSRF protection. ---- @Configuration @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()); + return http.build(); } } ---- @@ -146,14 +148,16 @@ public class WebSecurityConfig extends ---- @Configuration @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { csrf { disable() } } + return http.build() } } ---- @@ -330,15 +334,15 @@ For example, the following Java Configuration will perform logout with the URL ` [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) ); + return http.build(); } } ---- @@ -347,14 +351,16 @@ public class WebSecurityConfig extends [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { logout { logoutRequestMatcher = AntPathRequestMatcher("/logout") } } + return http.build() } } ---- @@ -375,7 +381,7 @@ For details, refer to the <> section. If a token does expire, you might want to customize how it is handled by specifying a custom `AccessDeniedHandler`. The custom `AccessDeniedHandler` can process the `InvalidCsrfTokenException` any way you like. -For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both xref:servlet/appendix/namespace.adoc#nsa-access-denied-handler[xml] and {gh-url}/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java#L64[Java configuration]. +For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both xref:servlet/appendix/namespace/http.adoc#nsa-access-denied-handler[xml] and {gh-url}/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java#L64[Java configuration]. // FIXME: We should add a custom AccessDeniedHandler section in the reference and update the links above diff --git a/docs/modules/ROOT/pages/servlet/exploits/headers.adoc b/docs/modules/ROOT/pages/servlet/exploits/headers.adoc index 535f3e976bb..1502b610730 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/headers.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/headers.adoc @@ -21,11 +21,10 @@ You can easily do this with the following Configuration: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -33,6 +32,7 @@ public class WebSecurityConfig extends .sameOrigin() ) ); + return http.build(); } } ---- @@ -53,8 +53,9 @@ public class WebSecurityConfig extends [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class SecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -63,6 +64,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -79,11 +81,10 @@ If you are using Spring Security's Configuration the following will only add xre [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -91,6 +92,7 @@ WebSecurityConfigurerAdapter { .defaultsDisabled() .cacheControl(withDefaults()) ); + return http.build(); } } ---- @@ -111,8 +113,9 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class SecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -122,6 +125,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -135,14 +139,14 @@ If necessary, you can disable all of the HTTP Security response headers with the [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers.disable()); + return http.build(); } } ---- @@ -161,14 +165,16 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class SecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { disable() } } + return http.build() } } ---- @@ -194,16 +200,16 @@ If necessary, you can also disable Spring Security's cache control HTTP response ---- @Configuration @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers .cacheControl(cache -> cache.disable()) ); + return http.build(); } } ---- @@ -224,9 +230,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { headers { cacheControl { @@ -234,6 +241,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -252,16 +260,16 @@ However, you can disable it with: ---- @Configuration @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers .contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable()) ); + return http.build(); } } ---- @@ -282,9 +290,10 @@ public class WebSecurityConfig extends [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { headers { contentTypeOptions { @@ -292,6 +301,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -310,11 +320,10 @@ For example, the following is an example of explicitly providing HSTS: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -324,6 +333,7 @@ WebSecurityConfigurerAdapter { .maxAgeInSeconds(31536000) ) ); + return http.build(); } } ---- @@ -347,9 +357,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { headers { httpStrictTransportSecurity { @@ -359,6 +370,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -376,11 +388,10 @@ You can enable HPKP headers with the following Configuration: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -390,6 +401,7 @@ WebSecurityConfigurerAdapter { .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=") ) ); + return http.build(); } } ---- @@ -416,9 +428,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { headers { httpPublicKeyPinning { @@ -429,6 +442,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -447,11 +461,10 @@ You can customize frame options to use the same origin within a Configuration us [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -459,6 +472,7 @@ WebSecurityConfigurerAdapter { .sameOrigin() ) ); + return http.build(); } } ---- @@ -481,9 +495,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { headers { frameOptions { @@ -491,6 +506,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -509,11 +525,10 @@ For example, the following Configuration specifies that Spring Security should n [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -521,6 +536,7 @@ WebSecurityConfigurerAdapter { .block(false) ) ); + return http.build(); } } ---- @@ -541,9 +557,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { // ... http { headers { @@ -552,6 +569,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -581,11 +599,10 @@ You can enable the CSP header as shown below: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -593,6 +610,7 @@ WebSecurityConfigurerAdapter { .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") ) ); + return http.build(); } } ---- @@ -614,9 +632,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -625,6 +644,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -638,11 +658,10 @@ To enable the CSP `report-only` header, provide the following configuration: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -651,6 +670,7 @@ public class WebSecurityConfig extends .reportOnly() ) ); + return http.build(); } } ---- @@ -673,9 +693,10 @@ public class WebSecurityConfig extends [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -685,6 +706,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -702,11 +724,10 @@ You can enable the Referrer Policy header using the configuration as shown below [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -714,6 +735,7 @@ WebSecurityConfigurerAdapter { .policy(ReferrerPolicy.SAME_ORIGIN) ) ); + return http.build(); } } ---- @@ -734,9 +756,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -745,6 +768,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -772,16 +796,16 @@ can enable the Feature Policy header using the configuration shown below: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers .featurePolicy("geolocation 'self'") ); + return http.build(); } } ---- @@ -802,15 +826,17 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { featurePolicy("geolocation 'self'") } } + return http.build() } } ---- @@ -838,11 +864,10 @@ can enable the Permissions Policy header using the configuration shown below: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -850,6 +875,7 @@ WebSecurityConfigurerAdapter { .policy("geolocation=(self)") ) ); + return http.build(); } } ---- @@ -870,9 +896,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -881,6 +908,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -907,15 +935,16 @@ can be sent on log out with the following configuration: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... - .logout() - .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(CACHE, COOKIES))); + .logout((logout) -> logout + .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(CACHE, COOKIES))) + ); + return http.build(); } } ---- @@ -924,20 +953,83 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... logout { addLogoutHandler(HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(CACHE, COOKIES))) } } + return http.build() } } ---- ==== +[[servlet-headers-cross-origin-policies]] +== Cross-Origin Policies + +Spring Security provides built-in support for adding some Cross-Origin policies headers, those headers are: + +[source] +---- +Cross-Origin-Opener-Policy +Cross-Origin-Embedder-Policy +Cross-Origin-Resource-Policy +---- + +Spring Security does not add <> headers by default. +The headers can be added with the following configuration: + +.Cross-Origin Policies +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) { + http.headers((headers) -> headers + .crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + .crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + .crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN))); + return http.build(); + } +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +open class CrossOriginPoliciesConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + headers { + crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN) + } + } + return http.build() + } +} +---- +==== + +This configuration will write the headers with the values provided: +[source] +---- +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: same-origin +---- + [[servlet-headers-custom]] == Custom Headers Spring Security has mechanisms to make it convenient to add the more common security headers to your application. @@ -961,16 +1053,16 @@ The headers could be added to the response using the following Configuration: [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value")) ); + return http.build(); } } ---- @@ -991,15 +1083,17 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { addHeaderWriter(StaticHeadersWriter("X-Custom-Security-Header","header-value")) } } + return http.build() } } ---- @@ -1018,16 +1112,16 @@ If you wanted to explicitly configure <> it could [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)) ); + return http.build(); } } ---- @@ -1054,15 +1148,17 @@ See https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsi [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { addHeaderWriter(XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)) } } + return http.build() } } ---- @@ -1083,11 +1179,10 @@ An example of using `DelegatingRequestMatcherHeaderWriter` in Java Configuration [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends -WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { RequestMatcher matcher = new AntPathRequestMatcher("/login"); DelegatingRequestMatcherHeaderWriter headerWriter = new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter()); @@ -1097,6 +1192,7 @@ WebSecurityConfigurerAdapter { .frameOptions(frameOptions -> frameOptions.disable()) .addHeaderWriter(headerWriter) ); + return http.build(); } } ---- @@ -1130,9 +1226,10 @@ WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { val matcher: RequestMatcher = AntPathRequestMatcher("/login") val headerWriter = DelegatingRequestMatcherHeaderWriter(matcher, XFrameOptionsHeaderWriter()) http { @@ -1143,6 +1240,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { addHeaderWriter(headerWriter) } } + return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/exploits/http.adoc b/docs/modules/ROOT/pages/servlet/exploits/http.adoc index 3dc10d8ac79..e167320e6ae 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/http.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/http.adoc @@ -19,16 +19,16 @@ For example, the following Java configuration will redirect any HTTP requests to ---- @Configuration @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .requiresChannel(channel -> channel .anyRequest().requiresSecure() ); + return http.build(); } } ---- @@ -38,15 +38,17 @@ public class WebSecurityConfig extends ---- @Configuration @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... requiresChannel { secure(AnyRequestMatcher.INSTANCE, "REQUIRES_SECURE_CHANNEL") } } + return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc b/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc new file mode 100644 index 00000000000..23bb319d7c7 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc @@ -0,0 +1,166 @@ +[[concurrency]] += Concurrency Support + +In most environments, Security is stored on a per `Thread` basis. +This means that when work is done on a new `Thread`, the `SecurityContext` is lost. +Spring Security provides some infrastructure to help make this much easier for users. +Spring Security provides low level abstractions for working with Spring Security in multi-threaded environments. +In fact, this is what Spring Security builds on to integration with xref:servlet/integrations/servlet-api.adoc#servletapi-start-runnable[`AsyncContext.start(Runnable)`] and xref:servlet/integrations/mvc.adoc#mvc-async[Spring MVC Async Integration]. + +== DelegatingSecurityContextRunnable + +One of the most fundamental building blocks within Spring Security's concurrency support is the `DelegatingSecurityContextRunnable`. +It wraps a delegate `Runnable` in order to initialize the `SecurityContextHolder` with a specified `SecurityContext` for the delegate. +It then invokes the delegate Runnable ensuring to clear the `SecurityContextHolder` afterwards. +The `DelegatingSecurityContextRunnable` looks something like this: + +[source,java] +---- +public void run() { +try { + SecurityContextHolder.setContext(securityContext); + delegate.run(); +} finally { + SecurityContextHolder.clearContext(); +} +} +---- + +While very simple, it makes it seamless to transfer the SecurityContext from one Thread to another. +This is important since, in most cases, the SecurityContextHolder acts on a per Thread basis. +For example, you might have used Spring Security's xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[``] support to secure one of your services. +You can now easily transfer the `SecurityContext` of the current `Thread` to the `Thread` that invokes the secured service. +An example of how you might do this can be found below: + +[source,java] +---- +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +SecurityContext context = SecurityContextHolder.getContext(); +DelegatingSecurityContextRunnable wrappedRunnable = + new DelegatingSecurityContextRunnable(originalRunnable, context); + +new Thread(wrappedRunnable).start(); +---- + +The code above performs the following steps: + +* Creates a `Runnable` that will be invoking our secured service. +Notice that it is not aware of Spring Security +* Obtains the `SecurityContext` that we wish to use from the `SecurityContextHolder` and initializes the `DelegatingSecurityContextRunnable` +* Use the `DelegatingSecurityContextRunnable` to create a Thread +* Start the Thread we created + +Since it is quite common to create a `DelegatingSecurityContextRunnable` with the `SecurityContext` from the `SecurityContextHolder` there is a shortcut constructor for it. +The following code is the same as the code above: + + +[source,java] +---- +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +DelegatingSecurityContextRunnable wrappedRunnable = + new DelegatingSecurityContextRunnable(originalRunnable); + +new Thread(wrappedRunnable).start(); +---- + +The code we have is simple to use, but it still requires knowledge that we are using Spring Security. +In the next section we will take a look at how we can utilize `DelegatingSecurityContextExecutor` to hide the fact that we are using Spring Security. + +== DelegatingSecurityContextExecutor + +In the previous section we found that it was easy to use the `DelegatingSecurityContextRunnable`, but it was not ideal since we had to be aware of Spring Security in order to use it. +Let's take a look at how `DelegatingSecurityContextExecutor` can shield our code from any knowledge that we are using Spring Security. + +The design of `DelegatingSecurityContextExecutor` is very similar to that of `DelegatingSecurityContextRunnable` except it accepts a delegate `Executor` instead of a delegate `Runnable`. +You can see an example of how it might be used below: + + +[source,java] +---- +SecurityContext context = SecurityContextHolder.createEmptyContext(); +Authentication authentication = + UsernamePasswordAuthenticationToken.authenticated("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER")); +context.setAuthentication(authentication); + +SimpleAsyncTaskExecutor delegateExecutor = + new SimpleAsyncTaskExecutor(); +DelegatingSecurityContextExecutor executor = + new DelegatingSecurityContextExecutor(delegateExecutor, context); + +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +executor.execute(originalRunnable); +---- + +The code performs the following steps: + +* Creates the `SecurityContext` to be used for our `DelegatingSecurityContextExecutor`. +Note that in this example we simply create the `SecurityContext` by hand. +However, it does not matter where or how we get the `SecurityContext` (i.e. we could obtain it from the `SecurityContextHolder` if we wanted). +* Creates a delegateExecutor that is in charge of executing submitted ``Runnable``s +* Finally we create a `DelegatingSecurityContextExecutor` which is in charge of wrapping any Runnable that is passed into the execute method with a `DelegatingSecurityContextRunnable`. +It then passes the wrapped Runnable to the delegateExecutor. +In this instance, the same `SecurityContext` will be used for every Runnable submitted to our `DelegatingSecurityContextExecutor`. +This is nice if we are running background tasks that need to be run by a user with elevated privileges. +* At this point you may be asking yourself "How does this shield my code of any knowledge of Spring Security?" Instead of creating the `SecurityContext` and the `DelegatingSecurityContextExecutor` in our own code, we can inject an already initialized instance of `DelegatingSecurityContextExecutor`. + +[source,java] +---- +@Autowired +private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor + +public void submitRunnable() { +Runnable originalRunnable = new Runnable() { + public void run() { + // invoke secured service + } +}; +executor.execute(originalRunnable); +} +---- + +Now our code is unaware that the `SecurityContext` is being propagated to the `Thread`, then the `originalRunnable` is run, and then the `SecurityContextHolder` is cleared out. +In this example, the same user is being used to run each thread. +What if we wanted to use the user from `SecurityContextHolder` at the time we invoked `executor.execute(Runnable)` (i.e. the currently logged in user) to process ``originalRunnable``? +This can be done by removing the `SecurityContext` argument from our `DelegatingSecurityContextExecutor` constructor. +For example: + + +[source,java] +---- +SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor(); +DelegatingSecurityContextExecutor executor = + new DelegatingSecurityContextExecutor(delegateExecutor); +---- + +Now anytime `executor.execute(Runnable)` is executed the `SecurityContext` is first obtained by the `SecurityContextHolder` and then that `SecurityContext` is used to create our `DelegatingSecurityContextRunnable`. +This means that we are running our `Runnable` with the same user that was used to invoke the `executor.execute(Runnable)` code. + +== Spring Security Concurrency Classes + +Refer to the Javadoc for additional integrations with both the Java concurrent APIs and the Spring Task abstractions. +They are quite self-explanatory once you understand the previous code. + +* `DelegatingSecurityContextCallable` +* `DelegatingSecurityContextExecutor` +* `DelegatingSecurityContextExecutorService` +* `DelegatingSecurityContextRunnable` +* `DelegatingSecurityContextScheduledExecutorService` +* `DelegatingSecurityContextSchedulingTaskExecutor` +* `DelegatingSecurityContextAsyncTaskExecutor` +* `DelegatingSecurityContextTaskExecutor` +* `DelegatingSecurityContextTaskScheduler` diff --git a/docs/modules/ROOT/pages/servlet/integrations/cors.adoc b/docs/modules/ROOT/pages/servlet/integrations/cors.adoc index aa5cb3ff172..39f7ac3df7d 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/cors.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/cors.adoc @@ -13,14 +13,15 @@ Users can integrate the `CorsFilter` with Spring Security by providing a `CorsCo [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // by default uses a Bean by the name of corsConfigurationSource .cors(withDefaults()) ... + return http.build(); } @Bean @@ -39,13 +40,15 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -open class WebSecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +open class WebSecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // by default uses a Bean by the name of corsConfigurationSource cors { } // ... } + return http.build() } @Bean @@ -81,15 +84,16 @@ If you are using Spring MVC's CORS support, you can omit specifying the `CorsCon [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // if Spring MVC is on classpath and no CorsConfigurationSource is provided, // Spring Security will use CORS configuration provided to Spring MVC .cors(withDefaults()) ... + return http.build(); } } ---- @@ -98,14 +102,16 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -open class WebSecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +open class WebSecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // if Spring MVC is on classpath and no CorsConfigurationSource is provided, // Spring Security will use CORS configuration provided to Spring MVC cors { } // ... } + return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/integrations/data.adoc b/docs/modules/ROOT/pages/servlet/integrations/data.adoc new file mode 100644 index 00000000000..214db256188 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/data.adoc @@ -0,0 +1,69 @@ +[[data]] += Spring Data Integration + +Spring Security provides Spring Data integration that allows referring to the current user within your queries. +It is not only useful but necessary to include the user in the queries to support paged results since filtering the results afterwards would not scale. + +[[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`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public SecurityEvaluationContextExtension securityEvaluationContextExtension() { + return new SecurityEvaluationContextExtension(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension { + return SecurityEvaluationContextExtension() +} +---- +==== + +In XML Configuration, this would look like: + +[source,xml] +---- + +---- + +[[data-query]] +== Security Expressions within @Query + +Now Spring Security can be used within your queries. +For example: + +==== +.Java +[source,java,role="primary"] +---- +@Repository +public interface MessageRepository extends PagingAndSortingRepository { + @Query("select m from Message m where m.to.id = ?#{ principal?.id }") + Page findInbox(Pageable pageable); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Repository +interface MessageRepository : PagingAndSortingRepository { + @Query("select m from Message m where m.to.id = ?#{ principal?.id }") + fun findInbox(pageable: Pageable): Page +} +---- +==== + +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. diff --git a/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc new file mode 100644 index 00000000000..ee17f37c694 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc @@ -0,0 +1,30 @@ +[[jackson]] += Jackson Support + +Spring Security provides Jackson support for persisting Spring Security related classes. +This can improve the performance of serializing Spring Security related classes when working with distributed sessions (i.e. session replication, Spring Session, etc). + +To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]): + +[source,java] +---- +ObjectMapper mapper = new ObjectMapper(); +ClassLoader loader = getClass().getClassLoader(); +List modules = SecurityJackson2Modules.getModules(loader); +mapper.registerModules(modules); + +// ... use ObjectMapper as normally ... +SecurityContext context = new SecurityContextImpl(); +// ... +String json = mapper.writeValueAsString(context); +---- + +[NOTE] +==== +The following Spring Security modules provide Jackson support: + +- spring-security-core (`CoreJackson2Module`) +- spring-security-web (`WebJackson2Module`, `WebServletJackson2Module`, `WebServerJackson2Module`) +- <> (`OAuth2ClientJackson2Module`) +- spring-security-cas (`CasJackson2Module`) +==== diff --git a/docs/modules/ROOT/pages/servlet/integrations/localization.adoc b/docs/modules/ROOT/pages/servlet/integrations/localization.adoc new file mode 100644 index 00000000000..e1fc22b9a25 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/localization.adoc @@ -0,0 +1,36 @@ +[[localization]] += Localization +Spring Security supports localization of exception messages that end users are likely to see. +If your application is designed for English-speaking users, you don't need to do anything as by default all Security messages are in English. +If you need to support other locales, everything you need to know is contained in this section. + +All exception messages can be localized, including messages related to authentication failures and access being denied (authorization failures). +Exceptions and logging messages that are focused on developers or system deplopers (including incorrect attributes, interface contract violations, using incorrect constructors, startup time validation, debug-level logging) are not localized and instead are hard-coded in English within Spring Security's code. + +Shipping in the `spring-security-core-xx.jar` you will find an `org.springframework.security` package that in turn contains a `messages.properties` file, as well as localized versions for some common languages. +This should be referred to by your `ApplicationContext`, as Spring Security classes implement Spring's `MessageSourceAware` interface and expect the message resolver to be dependency injected at application context startup time. +Usually all you need to do is register a bean inside your application context to refer to the messages. +An example is shown below: + +[source,xml] +---- + + + +---- + +The `messages.properties` is named in accordance with standard resource bundles and represents the default language supported by Spring Security messages. +This default file is in English. + +If you wish to customize the `messages.properties` file, or support other languages, you should copy the file, rename it accordingly, and register it inside the above bean definition. +There are not a large number of message keys inside this file, so localization should not be considered a major initiative. +If you do perform localization of this file, please consider sharing your work with the community by logging a JIRA task and attaching your appropriately-named localized version of `messages.properties`. + +Spring Security relies on Spring's localization support in order to actually lookup the appropriate message. +In order for this to work, you have to make sure that the locale from the incoming request is stored in Spring's `org.springframework.context.i18n.LocaleContextHolder`. +Spring MVC's `DispatcherServlet` does this for your application automatically, but since Spring Security's filters are invoked before this, the `LocaleContextHolder` needs to be set up to contain the correct `Locale` before the filters are called. +You can either do this in a filter yourself (which must come before the Spring Security filters in `web.xml`) or you can use Spring's `RequestContextFilter`. +Please refer to the Spring Framework documentation for further details on using localization with Spring. + +The "contacts" sample application is set up to use localized messages. diff --git a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc index 23bb16ccaca..212c545ccca 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc @@ -136,23 +136,27 @@ If we wanted to restrict access to this controller method to admin users, a deve .Java [source,java,role="primary"] ---- -protected configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .antMatchers("/admin").hasRole("ADMIN") ); + return http.build(); } ---- .Kotlin [source,kotlin,role="secondary"] ---- -override fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(AntPathRequestMatcher("/admin"), hasRole("ADMIN")) } } + return http.build() } ---- ==== @@ -181,23 +185,27 @@ The following configuration will protect the same URLs that Spring MVC will matc .Java [source,java,role="primary"] ---- -protected configure(HttpSecurity http) throws Exception { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/admin").hasRole("ADMIN") ); + // ... } ---- .Kotlin [source,kotlin,role="secondary"] ---- -override fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/admin", hasRole("ADMIN")) } } + // ... } ---- ==== diff --git a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc index 6ae9b57a86a..348bf54737e 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc @@ -11,24 +11,39 @@ This is because the format is unknown, so there is https://docs.spring.io/spring Additionally, JSR-356 does not provide a way to intercept messages, so security would be rather invasive. **** +[[websocket-authentication]] +== WebSocket Authentication + +WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made. +This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets. +If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically. + +More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application. + [[websocket-configuration]] -== WebSocket Configuration +== WebSocket Authorization Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction. -To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`. -For example: + +In Spring Security 5.8, this support has been refreshed to use the `AuthorizationManager` API. + +To configure authorization using Java Configuration, simply include the `@EnableWebSocketSecurity` annotation and publish an `AuthorizationManager>` bean or in XML use the `use-authorization-manager` attribute. +One way to do this is by using the `AuthorizationManagerMessageMatcherRegistry` to specify endpoint patterns like so: ==== .Java [source,java,role="primary"] ---- @Configuration -public class WebSocketSecurityConfig - extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2> +@EnableWebSocketSecurity // <1> <2> +public class WebSocketSecurityConfig { - protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { + @Bean + AuthorizationManager> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { messages .simpDestMatchers("/user/**").authenticated() // <3> + + return messages.build(); } } ---- @@ -37,30 +52,24 @@ public class WebSocketSecurityConfig [source,kotlin,role="secondary"] ---- @Configuration -open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2> - override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { +@EnableWebSocketSecurity // <1> <2> +open class WebSocketSecurityConfig { // <1> <2> + @Bean + fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { messages.simpDestMatchers("/user/**").authenticated() // <3> + return messages.build() } } ---- -==== - -This will ensure that: -<1> Any inbound CONNECT message requires a valid CSRF token to enforce <> -<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request. -<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <> - -Spring Security also provides xref:servlet/appendix/namespace.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. -A comparable XML based configuration looks like the following: - -[source,xml] +.Xml +[source,xml,role="secondary"] ---- - - - + + ---- +==== This will ensure that: @@ -68,31 +77,59 @@ This will ensure that: <2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request. <3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <> -[[websocket-authentication]] -== WebSocket Authentication +=== Custom Authorization -WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made. -This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets. -If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically. +When using `AuthorizationManager`, customization is quite simple. +For example, you can publish an `AuthorizationManager` that requires that all messages have a role of "USER" using `AuthorityAuthorizationManager`, as seen below: -More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application. +==== +.Java +[source,java,role="primary"] +---- +@Configuration +@EnableWebSocketSecurity // <1> <2> +public class WebSocketSecurityConfig { -[[websocket-authorization]] -== WebSocket Authorization + @Bean + AuthorizationManager> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { + return AuthorityAuthorizationManager.hasRole("USER"); + } +} +---- -Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction. -To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`. -For example: +.Kotlin +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableWebSocketSecurity // <1> <2> +open class WebSocketSecurityConfig { + @Bean + fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { + return AuthorityAuthorizationManager.hasRole("USER") // <3> + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + +---- +==== + +There are several ways to further match messages, as can be seen in a more advanced example below: ==== .Java [source,java,role="primary"] ---- @Configuration -public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { +public class WebSocketSecurityConfig { - @Override - protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { + @Bean + public AuthorizationManager> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { messages .nullDestMatcher().authenticated() // <1> .simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2> @@ -101,6 +138,7 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5> .anyMessage().denyAll(); // <6> + return messages.build(); } } ---- @@ -109,8 +147,8 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro [source,kotlin,role="secondary"] ---- @Configuration -open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { - override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { +open class WebSocketSecurityConfig { + fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager { messages .nullDestMatcher().authenticated() // <1> .simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2> @@ -118,26 +156,16 @@ open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfi .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4> .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5> .anyMessage().denyAll() // <6> + + return messages.build(); } } ---- -==== - -This will ensure that: - -<1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated -<2> Anyone can subscribe to /user/queue/errors -<3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER -<4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER -<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types. -<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages. - -Spring Security also provides xref:servlet/appendix/namespace.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. -A comparable XML based configuration looks like the following: -[source,xml] +.Xml +[source,kotlin,role="secondary"] ---- - + @@ -147,8 +175,8 @@ A comparable XML based configuration looks like the following: - - + + @@ -157,15 +185,16 @@ A comparable XML based configuration looks like the following: ---- +==== This will ensure that: -<1> Any message of type CONNECT, UNSUBSCRIBE, or DISCONNECT will require the user to be authenticated +<1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated <2> Anyone can subscribe to /user/queue/errors <3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER <4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER <5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types. -<6> Any other message with a destination is rejected. This is a good idea to ensure that you do not miss any messages. +<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages. [[websocket-authorization-notes]] === WebSocket Authorization Notes @@ -311,8 +340,64 @@ stompClient.connect(headers, function(frame) { [[websocket-sameorigin-disable]] === Disable CSRF within WebSockets +NOTE: At this point, CSRF is not configurable when using `@EnableWebSocketSecurity`, though this will likely be added in a future release. + +To disable CSRF, instead of using `@EnableWebSocketSecurity`, you can use XML support or add the Spring Security components yourself, like so: + +==== +.Java +[source,java,role="primary"] +---- +@Configuration +public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + AuthorizationManager> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated(); + AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules); + AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context); + authz.setAuthorizationEventPublisher(publisher); + registration.interceptors(new SecurityContextChannelInterceptor(), authz); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Configuration +open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer { + @Override + override fun addArgumentResolvers(argumentResolvers: List) { + argumentResolvers.add(AuthenticationPrincipalArgumentResolver()) + } + + @Override + override fun configureClientInboundChannel(registration: ChannelRegistration) { + var myAuthorizationRules: AuthorizationManager> = AuthenticatedAuthorizationManager.authenticated() + var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules) + var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context) + authz.setAuthorizationEventPublisher(publisher) + registration.interceptors(SecurityContextChannelInterceptor(), authz) + } +} +---- -If you want to allow other domains to access your site, you can disable Spring Security's protection. +.Xml +[source,xml,role="secondary"] +---- + + + +---- +==== + +On the other hand, if you are using the <> and you want to allow other domains to access your site, you can disable Spring Security's protection. For example, in Java Configuration you can use the following: ==== @@ -346,6 +431,39 @@ open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfi ---- ==== +[[websocket-expression-handler]] +=== Custom Expression Handler + +At times, there may be value in customizing how the `access` expressions are handled defined in your `intercept-message` XML elements. +To do this, you can create a class of type `SecurityExpressionHandler>` and refer to it in your XML definition like so: + +[source,xml] +---- + + + ... + + + +---- + +If you are migrating from a legacy usage of `websocket-message-broker` that implements a `SecurityExpressionHandler>`, you can: + 1. Additionally implement the `createEvaluationContext(Supplier, Message)` method and then + 2. Wrap that value in a `MessageAuthorizationContextSecurityExpressionHandler` like so: + +[source,xml] +---- + + + ... + + + + + + + +---- [[websocket-sockjs]] == Working with SockJS @@ -360,7 +478,7 @@ SockJS may use an https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport By default Spring Security will xref:features/exploits/headers.adoc#headers-frame-options[deny] the site from being framed to prevent Clickjacking attacks. To allow SockJS frame based transports to work, we need to configure Spring Security to allow the same origin to frame the content. -You can customize X-Frame-Options with the xref:servlet/appendix/namespace.adoc#nsa-frame-options[frame-options] element. +You can customize X-Frame-Options with the xref:servlet/appendix/namespace/http.adoc#nsa-frame-options[frame-options] element. For example, the following will instruct Spring Security to use "X-Frame-Options: SAMEORIGIN" which allows iframes within the same domain: [source,xml] @@ -382,11 +500,10 @@ Similarly, you can customize frame options to use the same origin within Java Co [source,java,role="primary"] ---- @EnableWebSecurity -public class WebSecurityConfig extends - WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .headers(headers -> headers @@ -394,6 +511,7 @@ public class WebSecurityConfig extends .sameOrigin() ) ); + return http.build(); } } ---- @@ -402,8 +520,9 @@ public class WebSecurityConfig extends [source,kotlin,role="secondary"] ---- @EnableWebSecurity -open class WebSecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +open class WebSecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... headers { @@ -412,6 +531,7 @@ open class WebSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -440,11 +560,10 @@ For example, if our stomp endpoint is "/chat" we can disable CSRF protection for ---- @Configuration @EnableWebSecurity -public class WebSecurityConfig - extends WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf // ignore our stomp endpoints since they are protected using Stomp headers @@ -456,7 +575,7 @@ public class WebSecurityConfig .sameOrigin() ) ) - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize ... ) ... @@ -467,8 +586,9 @@ public class WebSecurityConfig ---- @Configuration @EnableWebSecurity -open class WebSecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +open class WebSecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { csrf { ignoringAntMatchers("/chat/**") @@ -486,7 +606,7 @@ open class WebSecurityConfig : WebSecurityConfigurerAdapter() { ---- ==== -If we are using XML based configuration, we can use the xref:servlet/appendix/namespace.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref]. +If we are using XML based configuration, we can use the xref:servlet/appendix/namespace/http.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref]. For example: [source,xml] @@ -513,3 +633,47 @@ For example: ---- + +[[legacy-websocket-configuration]] +== Legacy WebSocket Configuration + +Before Spring Security 5.8, the way to configure messaging authorization using Java Configuration, was to extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`. +For example: + +==== +.Java +[source,java,role="primary"] +---- +@Configuration +public class WebSocketSecurityConfig + extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2> + + protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { + messages + .simpDestMatchers("/user/**").authenticated() // <3> + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Configuration +open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2> + override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { + messages.simpDestMatchers("/user/**").authenticated() // <3> + } +} +---- +==== + +This will ensure that: + +<1> Any inbound CONNECT message requires a valid CSRF token to enforce <> +<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request. +<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <> + +Using the legacy configuration is helpful in the event that you have a custom `SecurityExpressionHandler` that extends `AbstractSecurityExpressionHandler` and overrides `createEvaluationContextInternal` or `createSecurityExpressionRoot`. +In order to defer `Authorization` lookup, the new `AuthorizationManager` API does not invoke these when evaluating expressions. + +If you are using XML, you can use the legacy APIs simply by not using the `use-authorization-manager` element or setting it to `false`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc similarity index 52% rename from docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc rename to docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc index c02a4a0bc65..aea17b02a99 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc @@ -1,630 +1,21 @@ -[[oauth2client]] -= OAuth 2.0 Client - -The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. - -At a high-level, the core features available are: - -.Authorization Grant support -* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] -* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] -* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] -* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] - -.Client Authentication support -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] - -.HTTP Client support -* <> (for requesting protected resources) - -The `HttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. -In addition, `HttpSecurity.oauth2Client().authorizationCodeGrant()` enables the customization of the Authorization Code grant. - -The following code shows the complete configuration options provided by the `HttpSecurity.oauth2Client()` DSL: - -.OAuth2 Client Configuration Options -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .oauth2Client(oauth2 -> oauth2 - .clientRegistrationRepository(this.clientRegistrationRepository()) - .authorizedClientRepository(this.authorizedClientRepository()) - .authorizedClientService(this.authorizedClientService()) - .authorizationCodeGrant(codeGrant -> codeGrant - .authorizationRequestRepository(this.authorizationRequestRepository()) - .authorizationRequestResolver(this.authorizationRequestResolver()) - .accessTokenResponseClient(this.accessTokenResponseClient()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http { - oauth2Client { - clientRegistrationRepository = clientRegistrationRepository() - authorizedClientRepository = authorizedClientRepository() - authorizedClientService = authorizedClientService() - authorizationCodeGrant { - authorizationRequestRepository = authorizationRequestRepository() - authorizationRequestResolver = authorizationRequestResolver() - accessTokenResponseClient = accessTokenResponseClient() - } - } - } - } -} ----- -==== - -In addition to the `HttpSecurity.oauth2Client()` DSL, XML configuration is also supported. - -The following code shows the complete configuration options available in the xref:servlet/appendix/namespace.adoc#nsa-oauth2-client[ security namespace]: - -.OAuth2 Client XML Configuration Options -==== -[source,xml] ----- - - - - - ----- -==== - -The `OAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `OAuth2AuthorizedClientProvider`(s). - -The following code shows an example of how to register an `OAuth2AuthorizedClientManager` `@Bean` and associate it with an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { - val authorizedClientProvider: OAuth2AuthorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -The following sections will go into more detail on the core components used by OAuth 2.0 Client and the configuration options available: - -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -* <> -** <> -* <> - - -[[oauth2Client-core-interface-class]] -== Core Interfaces / Classes - - -[[oauth2Client-client-registration]] -=== ClientRegistration - -`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - -A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. - -`ClientRegistration` and its properties are defined as follows: - -[source,java] ----- -public final class ClientRegistration { - private String registrationId; <1> - private String clientId; <2> - private String clientSecret; <3> - private ClientAuthenticationMethod clientAuthenticationMethod; <4> - private AuthorizationGrantType authorizationGrantType; <5> - private String redirectUri; <6> - private Set scopes; <7> - private ProviderDetails providerDetails; - private String clientName; <8> - - public class ProviderDetails { - private String authorizationUri; <9> - private String tokenUri; <10> - private UserInfoEndpoint userInfoEndpoint; - private String jwkSetUri; <11> - private String issuerUri; <12> - private Map configurationMetadata; <13> - - public class UserInfoEndpoint { - private String uri; <14> - private AuthenticationMethod authenticationMethod; <15> - private String userNameAttributeName; <16> - - } - } -} ----- -<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. -<2> `clientId`: The client identifier. -<3> `clientSecret`: The client secret. -<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. -The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. -<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. - The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. -<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent - to after the end-user has authenticated and authorized access to the client. -<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. -<8> `clientName`: A descriptive name used for the client. -The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. -<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. -<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. -<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, - which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. -<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. -<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. - This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. -<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. -<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. -The supported values are *header*, *form* and *query*. -<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. - -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. - -`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: - -==== -.Java -[source,java,role="primary"] ----- -ClientRegistration clientRegistration = - ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() ----- -==== - -The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. - -As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. - -[[oauth2Client-client-registration-repo]] -=== ClientRegistrationRepository - -The `ClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). - -[NOTE] -Client registration information is ultimately stored and owned by the associated Authorization Server. -This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. - -Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ClientRegistrationRepository`. - -[NOTE] -The default implementation of `ClientRegistrationRepository` is `InMemoryClientRegistrationRepository`. - -The auto-configuration also registers the `ClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; - - @GetMapping("/") - public String index() { - ClientRegistration oktaRegistration = - this.clientRegistrationRepository.findByRegistrationId("okta"); - - ... - - return "index"; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var clientRegistrationRepository: ClientRegistrationRepository - - @GetMapping("/") - fun index(): String { - val oktaRegistration = - this.clientRegistrationRepository.findByRegistrationId("okta") - - //... - - return "index"; - } -} ----- -==== - -[[oauth2Client-authorized-client]] -=== OAuth2AuthorizedClient - -`OAuth2AuthorizedClient` is a representation of an Authorized Client. -A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. - -`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. - - -[[oauth2Client-authorized-repo-service]] -=== OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService - -`OAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. -Whereas, the primary role of `OAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. - -From a developer perspective, the `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private OAuth2AuthorizedClientService authorizedClientService; - - @GetMapping("/") - public String index(Authentication authentication) { - OAuth2AuthorizedClient authorizedClient = - this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); - - OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); - - ... - - return "index"; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var authorizedClientService: OAuth2AuthorizedClientService - - @GetMapping("/") - fun index(authentication: Authentication): String { - val authorizedClient: OAuth2AuthorizedClient = - this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); - val accessToken = authorizedClient.accessToken - - ... - - return "index"; - } -} ----- -==== - -[NOTE] -Spring Boot 2.x auto-configuration registers an `OAuth2AuthorizedClientRepository` and/or `OAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. -However, the application may choose to override and register a custom `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` `@Bean`. - -The default implementation of `OAuth2AuthorizedClientService` is `InMemoryOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. - -Alternatively, the JDBC implementation `JdbcOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. - -[NOTE] -`JdbcOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. - - -[[oauth2Client-authorized-manager-provider]] -=== OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider - -The `OAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). - -The primary responsibilities include: - -* Authorizing (or re-authorizing) an OAuth 2.0 Client, using an `OAuth2AuthorizedClientProvider`. -* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using an `OAuth2AuthorizedClientService` or `OAuth2AuthorizedClientRepository`. -* Delegating to an `OAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). -* Delegating to an `OAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). - -An `OAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. -Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. - -The default implementation of `OAuth2AuthorizedClientManager` is `DefaultOAuth2AuthorizedClientManager`, which is associated with an `OAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. -The `OAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. - -The following code shows an example of how to configure and build an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -When an authorization attempt succeeds, the `DefaultOAuth2AuthorizedClientManager` will delegate to the `OAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `OAuth2AuthorizedClientRepository`. -In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `OAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientOAuth2AuthorizationFailureHandler`. -The default behaviour may be customized via `setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)`. - -The `DefaultOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. -This can be useful when you need to supply an `OAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. - -The following code shows an example of the `contextAttributesMapper`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, - // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); - - return authorizedClientManager; -} - -private Function> contextAttributesMapper() { - return authorizeRequest -> { - Map contextAttributes = Collections.emptyMap(); - HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); - String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME); - String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = new HashMap<>(); - - // `PasswordOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); - contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); - } - return contextAttributes; - }; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build() - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - - // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, - // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) - return authorizedClientManager -} - -private fun contextAttributesMapper(): Function> { - return Function { authorizeRequest -> - var contextAttributes: MutableMap = mutableMapOf() - val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name) - val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME) - val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD) - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = hashMapOf() - - // `PasswordOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username - contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password - } - contextAttributes - } -} ----- -==== - -The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`. -When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead. - -A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`. -Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. -An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. - -The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientService authorizedClientService) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build(); - - AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = - new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build() - val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - - [[oauth2Client-auth-grant-support]] -== Authorization Grant Support += Authorization Grant Support [[oauth2Client-auth-code-grant]] -=== Authorization Code +== Authorization Code [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant. -==== Obtaining Authorization +=== Obtaining Authorization [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.1.1[Authorization Request/Response] protocol flow for the Authorization Code grant. -==== Initiating the Authorization Request +=== Initiating the Authorization Request The `OAuth2AuthorizationRequestRedirectFilter` uses an `OAuth2AuthorizationRequestResolver` to resolve an `OAuth2AuthorizationRequest` and initiate the Authorization Code grant flow by redirecting the end-user's user-agent to the Authorization Server's Authorization Endpoint. @@ -681,6 +72,9 @@ If the client is running in an untrusted environment (eg. native application or . `client-secret` is omitted (or empty) . `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`) +[TIP] +If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`. + [[oauth2Client-auth-code-redirect-uri]] The `DefaultOAuth2AuthorizationRequestResolver` also supports `URI` template variables for the `redirect-uri` using `UriComponentsBuilder`. @@ -705,7 +99,7 @@ spring: Configuring the `redirect-uri` with `URI` template variables is especially useful when the OAuth 2.0 Client is running behind a xref:features/exploits/http.adoc#http-proxy-server[Proxy Server]. This ensures that the `X-Forwarded-*` headers are used when expanding the `redirect-uri`. -==== Customizing the Authorization Request +=== Customizing the Authorization Request One of the primary use cases an `OAuth2AuthorizationRequestResolver` can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework. @@ -722,15 +116,15 @@ The following example shows how to configure the `DefaultOAuth2AuthorizationRequ [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { @Autowired private ClientRegistrationRepository clientRegistrationRepository; - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 @@ -740,6 +134,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ) ) ); + return http.build(); } private OAuth2AuthorizationRequestResolver authorizationRequestResolver( @@ -765,12 +160,13 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class SecurityConfig : WebSecurityConfigurerAdapter() { +class SecurityConfig { @Autowired private lateinit var customClientRegistrationRepository: ClientRegistrationRepository - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -781,6 +177,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } private fun authorizationRequestResolver( @@ -852,7 +249,7 @@ private fun authorizationRequestCustomizer(): Consumer oauth2 .authorizationCodeGrant(codeGrant -> codeGrant @@ -880,6 +277,7 @@ public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } } ---- @@ -888,9 +286,10 @@ public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2ClientSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Client { authorizationCodeGrant { @@ -898,6 +297,7 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -913,7 +313,7 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { ---- ==== -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.1.3[Access Token Request/Response] protocol flow for the Authorization Code grant. @@ -923,16 +323,21 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Authoriz The `DefaultAuthorizationCodeTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2AuthorizationCodeGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.1.3[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2AuthorizationCodeGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2AuthorizationCodeGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultAuthorizationCodeTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -975,10 +380,10 @@ Whether you customize `DefaultAuthorizationCodeTokenResponseClient` or provide y [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2ClientSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Client(oauth2 -> oauth2 .authorizationCodeGrant(codeGrant -> codeGrant @@ -986,6 +391,7 @@ public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } } ---- @@ -994,9 +400,10 @@ public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2ClientSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Client { authorizationCodeGrant { @@ -1004,6 +411,7 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -1021,13 +429,13 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2Client-refresh-token-grant]] -=== Refresh Token +== Refresh Token [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.5[Refresh Token]. -==== Refreshing an Access Token +=== Refreshing an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-6[Access Token Request/Response] protocol flow for the Refresh Token grant. @@ -1037,16 +445,21 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Refresh The `DefaultRefreshTokenTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultRefreshTokenTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2RefreshTokenGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-6[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2RefreshTokenGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2RefreshTokenGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultRefreshTokenTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1127,13 +540,13 @@ If the `OAuth2AuthorizedClient.getRefreshToken()` is available and the `OAuth2Au [[oauth2Client-client-creds-grant]] -=== Client Credentials +== Client Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.4.2[Access Token Request/Response] protocol flow for the Client Credentials grant. @@ -1143,16 +556,21 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Client C The `DefaultClientCredentialsTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2ClientCredentialsGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2ClientCredentialsGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2ClientCredentialsGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultClientCredentialsTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1226,7 +644,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()` configures a `ClientCredentialsOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Client Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1361,13 +779,13 @@ If not provided, it will default to `ServletRequestAttributes` using `RequestCon [[oauth2Client-password-grant]] -=== Resource Owner Password Credentials +== Resource Owner Password Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.3.2[Access Token Request/Response] protocol flow for the Resource Owner Password Credentials grant. @@ -1377,16 +795,21 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Resource The `DefaultPasswordTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultPasswordTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2PasswordGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.3.2[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2PasswordGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2PasswordGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultPasswordTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1461,7 +884,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `OAuth2AuthorizedClientProviderBuilder.builder().password()` configures a `PasswordOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Resource Owner Password Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1639,13 +1062,13 @@ If not provided, it will default to `ServletRequestAttributes` using `RequestCon [[oauth2Client-jwt-bearer-grant]] -=== JWT Bearer +== JWT Bearer [NOTE] Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on the https://datatracker.ietf.org/doc/html/rfc7523[JWT Bearer] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[Access Token Request/Response] protocol flow for the JWT Bearer grant. @@ -1655,14 +1078,19 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the JWT Bear The `DefaultJwtBearerTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultJwtBearerTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `JwtBearerGrantRequestEntityConverter` builds a `RequestEntity` representation of a https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `JwtBearerGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `JwtBearerGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. -==== Customizing the Access Token Response + +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultJwtBearerTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1738,7 +1166,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) ---- ==== -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1855,436 +1283,8 @@ class OAuth2ResourceServerController { ---- ==== - -[[oauth2Client-client-auth-support]] -== Client Authentication Support - - -[[oauth2Client-jwt-bearer-auth]] -=== JWT Bearer - [NOTE] -Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. - -The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, -which is a `Converter` that customizes the Token Request parameters by adding -a signed JSON Web Token (JWS) in the `client_assertion` parameter. - -The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS -is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. - - -==== Authenticate using `private_key_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-authentication-method: private_key_jwt - authorization-grant-type: authorization_code - ... ----- - -The following example shows how to configure `DefaultAuthorizationCodeTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - RSAPublicKey publicKey = ... - RSAPrivateKey privateKey = ... - return new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = - new OAuth2AuthorizationCodeGrantRequestEntityConverter(); -requestEntityConverter.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); - -DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = - new DefaultAuthorizationCodeTokenResponseClient(); -tokenResponseClient.setRequestEntityConverter(requestEntityConverter); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver: Function = - Function { clientRegistration -> - if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - var publicKey: RSAPublicKey - var privateKey: RSAPrivateKey - RSAKey.Builder(publicKey) = //... - .privateKey(privateKey) = //... - .keyID(UUID.randomUUID().toString()) - .build() - } - null - } - -val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter() -requestEntityConverter.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) - -val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() -tokenResponseClient.setRequestEntityConverter(requestEntityConverter) ----- -==== - - -==== Authenticate using `client_secret_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - client-authentication-method: client_secret_jwt - authorization-grant-type: client_credentials - ... ----- - -The following example shows how to configure `DefaultClientCredentialsTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { - SecretKeySpec secretKey = new SecretKeySpec( - clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), - "HmacSHA256"); - return new OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter = - new OAuth2ClientCredentialsGrantRequestEntityConverter(); -requestEntityConverter.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); - -DefaultClientCredentialsTokenResponseClient tokenResponseClient = - new DefaultClientCredentialsTokenResponseClient(); -tokenResponseClient.setRequestEntityConverter(requestEntityConverter); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver = Function { clientRegistration: ClientRegistration -> - if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { - val secretKey = SecretKeySpec( - clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), - "HmacSHA256" - ) - OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build() - } - null -} - -val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter() -requestEntityConverter.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) - -val tokenResponseClient = DefaultClientCredentialsTokenResponseClient() -tokenResponseClient.setRequestEntityConverter(requestEntityConverter) ----- -==== - - -[[oauth2Client-additional-features]] -== Additional Features - - -[[oauth2Client-registered-authorized-client]] -=== Resolving an Authorized Client - -The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. -This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `OAuth2AuthorizedClientManager` or `OAuth2AuthorizedClientService`. - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @GetMapping("/") - public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); - - ... - - return "index"; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - @GetMapping("/") - fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { - val accessToken = authorizedClient.accessToken - - ... - - return "index" - } -} ----- -==== - -The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses an <> and therefore inherits it's capabilities. - - -[[oauth2Client-webclient-servlet]] -== WebClient integration for Servlet Environments - -The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. - -The `ServletOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. -It directly uses an <> and therefore inherits the following capabilities: - -* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. -** `authorization_code` - triggers the Authorization Request redirect to initiate the flow -** `client_credentials` - the access token is obtained directly from the Token Endpoint -** `password` - the access token is obtained directly from the Token Endpoint -* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if an `OAuth2AuthorizedClientProvider` is available to perform the authorization - -The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { - val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build() -} ----- -==== - -=== Providing the Authorized Client - -The `ServletOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). - -The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - String resourceUri = ... - - String body = webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono(String.class) - .block(); - - ... - - return "index"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { - val resourceUri: String = ... - val body: String = webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono() - .block() - - ... - - return "index" -} ----- -==== - -<1> `oauth2AuthorizedClient()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. +`JwtBearerOAuth2AuthorizedClientProvider` resolves the `Jwt` assertion via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example. -The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public String index() { - String resourceUri = ... - - String body = webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono(String.class) - .block(); - - ... - - return "index"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(): String { - val resourceUri: String = ... - - val body: String = webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono() - .block() - - ... - - return "index" -} ----- -==== -<1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. - - -=== Defaulting the Authorized Client - -If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServletOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. - -If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `HttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultOAuth2AuthorizedClient(true); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { - val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultOAuth2AuthorizedClient(true) - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. - -Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultClientRegistrationId("okta"); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { - val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultClientRegistrationId("okta") - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. +[TIP] +If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc new file mode 100644 index 00000000000..16a626dd737 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc @@ -0,0 +1,264 @@ +[[oauth2Client-additional-features]] += Authorized Client Features + +[[oauth2Client-registered-authorized-client]] +== Resolving an Authorized Client + +The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. +This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `OAuth2AuthorizedClientManager` or `OAuth2AuthorizedClientService`. + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @GetMapping("/") + public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + + ... + + return "index"; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + @GetMapping("/") + fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { + val accessToken = authorizedClient.accessToken + + ... + + return "index" + } +} +---- +==== + +The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses an xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[`OAuth2AuthorizedClientManager`] and therefore inherits it's capabilities. + + +[[oauth2Client-webclient-servlet]] +== WebClient integration for Servlet Environments + +The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. + +The `ServletOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. +It directly uses an xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[`OAuth2AuthorizedClientManager`] and therefore inherits the following capabilities: + +* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. +** `authorization_code` - triggers the Authorization Request redirect to initiate the flow +** `client_credentials` - the access token is obtained directly from the Token Endpoint +** `password` - the access token is obtained directly from the Token Endpoint +* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if an `OAuth2AuthorizedClientProvider` is available to perform the authorization + +The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + +=== Providing the Authorized Client + +The `ServletOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). + +The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + String resourceUri = ... + + String body = webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono(String.class) + .block(); + + ... + + return "index"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { + val resourceUri: String = ... + val body: String = webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== + +<1> `oauth2AuthorizedClient()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. + +The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public String index() { + String resourceUri = ... + + String body = webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono(String.class) + .block(); + + ... + + return "index"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(): String { + val resourceUri: String = ... + + val body: String = webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== +<1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. + + +=== Defaulting the Authorized Client + +If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServletOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. + +If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `HttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultOAuth2AuthorizedClient(true); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultOAuth2AuthorizedClient(true) + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. + +Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultClientRegistrationId("okta"); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId("okta") + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc new file mode 100644 index 00000000000..566055a1dd6 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc @@ -0,0 +1,197 @@ +[[oauth2Client-client-auth-support]] += Client Authentication Support + + +[[oauth2Client-jwt-bearer-auth]] +== JWT Bearer + +[NOTE] +Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. + +The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, +which is a `Converter` that customizes the Token Request parameters by adding +a signed JSON Web Token (JWS) in the `client_assertion` parameter. + +The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS +is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. + + +=== Authenticate using `private_key_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-authentication-method: private_key_jwt + authorization-grant-type: authorization_code + ... +---- + +The following example shows how to configure `DefaultAuthorizationCodeTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + RSAPublicKey publicKey = ... + RSAPrivateKey privateKey = ... + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = + new OAuth2AuthorizationCodeGrantRequestEntityConverter(); +requestEntityConverter.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); + +DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = + new DefaultAuthorizationCodeTokenResponseClient(); +tokenResponseClient.setRequestEntityConverter(requestEntityConverter); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver: Function = + Function { clientRegistration -> + if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + var publicKey: RSAPublicKey + var privateKey: RSAPrivateKey + RSAKey.Builder(publicKey) = //... + .privateKey(privateKey) = //... + .keyID(UUID.randomUUID().toString()) + .build() + } + null + } + +val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter() +requestEntityConverter.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) + +val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() +tokenResponseClient.setRequestEntityConverter(requestEntityConverter) +---- +==== + + +=== Authenticate using `client_secret_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + client-authentication-method: client_secret_jwt + authorization-grant-type: client_credentials + ... +---- + +The following example shows how to configure `DefaultClientCredentialsTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + SecretKeySpec secretKey = new SecretKeySpec( + clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), + "HmacSHA256"); + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter = + new OAuth2ClientCredentialsGrantRequestEntityConverter(); +requestEntityConverter.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); + +DefaultClientCredentialsTokenResponseClient tokenResponseClient = + new DefaultClientCredentialsTokenResponseClient(); +tokenResponseClient.setRequestEntityConverter(requestEntityConverter); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver = Function { clientRegistration: ClientRegistration -> + if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { + val secretKey = SecretKeySpec( + clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), + "HmacSHA256" + ) + OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null +} + +val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter() +requestEntityConverter.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) + +val tokenResponseClient = DefaultClientCredentialsTokenResponseClient() +tokenResponseClient.setRequestEntityConverter(requestEntityConverter) +---- +==== + +=== Customizing the JWT assertion + +The JWT produced by `NimbusJwtClientAuthenticationParametersConverter` contains the `iss`, `sub`, `aud`, `jti`, `iat` and `exp` claims by default. You can customize the headers and/or claims by providing a `Consumer>` to `setJwtClientAssertionCustomizer()`. The following example shows how to customize claims of the JWT: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = ... + +NimbusJwtClientAuthenticationParametersConverter converter = + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver); +converter.setJwtClientAssertionCustomizer((context) -> { + context.getHeaders().header("custom-header", "header-value"); + context.getClaims().claim("custom-claim", "claim-value"); +}); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver = ... + +val converter: NimbusJwtClientAuthenticationParametersConverter = + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +converter.setJwtClientAssertionCustomizer { context -> + context.headers.header("custom-header", "header-value") + context.claims.claim("custom-claim", "claim-value") +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc new file mode 100644 index 00000000000..e02d387d1fd --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc @@ -0,0 +1,440 @@ +[[oauth2Client-core-interface-class]] += Core Interfaces / Classes + + +[[oauth2Client-client-registration]] +== ClientRegistration + +`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + +A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. + +`ClientRegistration` and its properties are defined as follows: + +[source,java] +---- +public final class ClientRegistration { + private String registrationId; <1> + private String clientId; <2> + private String clientSecret; <3> + private ClientAuthenticationMethod clientAuthenticationMethod; <4> + private AuthorizationGrantType authorizationGrantType; <5> + private String redirectUri; <6> + private Set scopes; <7> + private ProviderDetails providerDetails; + private String clientName; <8> + + public class ProviderDetails { + private String authorizationUri; <9> + private String tokenUri; <10> + private UserInfoEndpoint userInfoEndpoint; + private String jwkSetUri; <11> + private String issuerUri; <12> + private Map configurationMetadata; <13> + + public class UserInfoEndpoint { + private String uri; <14> + private AuthenticationMethod authenticationMethod; <15> + private String userNameAttributeName; <16> + + } + } +} +---- +<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. +<2> `clientId`: The client identifier. +<3> `clientSecret`: The client secret. +<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. +The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. +<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. + The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. +<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent + to after the end-user has authenticated and authorized access to the client. +<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. +<8> `clientName`: A descriptive name used for the client. +The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. +<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. +<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. +<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, + which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. +<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. +<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. + This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. +<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. +<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. +The supported values are *header*, *form* and *query*. +<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. + +`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: + +==== +.Java +[source,java,role="primary"] +---- +ClientRegistration clientRegistration = + ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() +---- +==== + +The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. + +As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. + +[[oauth2Client-client-registration-repo]] +== ClientRegistrationRepository + +The `ClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). + +[NOTE] +Client registration information is ultimately stored and owned by the associated Authorization Server. +This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. + +Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ClientRegistrationRepository`. + +[NOTE] +The default implementation of `ClientRegistrationRepository` is `InMemoryClientRegistrationRepository`. + +The auto-configuration also registers the `ClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @GetMapping("/") + public String index() { + ClientRegistration oktaRegistration = + this.clientRegistrationRepository.findByRegistrationId("okta"); + + ... + + return "index"; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var clientRegistrationRepository: ClientRegistrationRepository + + @GetMapping("/") + fun index(): String { + val oktaRegistration = + this.clientRegistrationRepository.findByRegistrationId("okta") + + //... + + return "index"; + } +} +---- +==== + +[[oauth2Client-authorized-client]] +== OAuth2AuthorizedClient + +`OAuth2AuthorizedClient` is a representation of an Authorized Client. +A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. + +`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. + + +[[oauth2Client-authorized-repo-service]] +== OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService + +`OAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. +Whereas, the primary role of `OAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. + +From a developer perspective, the `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private OAuth2AuthorizedClientService authorizedClientService; + + @GetMapping("/") + public String index(Authentication authentication) { + OAuth2AuthorizedClient authorizedClient = + this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); + + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + + ... + + return "index"; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientService: OAuth2AuthorizedClientService + + @GetMapping("/") + fun index(authentication: Authentication): String { + val authorizedClient: OAuth2AuthorizedClient = + this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); + val accessToken = authorizedClient.accessToken + + ... + + return "index"; + } +} +---- +==== + +[NOTE] +Spring Boot 2.x auto-configuration registers an `OAuth2AuthorizedClientRepository` and/or `OAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. +However, the application may choose to override and register a custom `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` `@Bean`. + +The default implementation of `OAuth2AuthorizedClientService` is `InMemoryOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. + +Alternatively, the JDBC implementation `JdbcOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. + +[NOTE] +`JdbcOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. + + +[[oauth2Client-authorized-manager-provider]] +== OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider + +The `OAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). + +The primary responsibilities include: + +* Authorizing (or re-authorizing) an OAuth 2.0 Client, using an `OAuth2AuthorizedClientProvider`. +* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using an `OAuth2AuthorizedClientService` or `OAuth2AuthorizedClientRepository`. +* Delegating to an `OAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). +* Delegating to an `OAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). + +An `OAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. +Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. + +The default implementation of `OAuth2AuthorizedClientManager` is `DefaultOAuth2AuthorizedClientManager`, which is associated with an `OAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. +The `OAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. + +The following code shows an example of how to configure and build an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + +When an authorization attempt succeeds, the `DefaultOAuth2AuthorizedClientManager` will delegate to the `OAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `OAuth2AuthorizedClientRepository`. +In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `OAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientOAuth2AuthorizationFailureHandler`. +The default behaviour may be customized via `setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)`. + +The `DefaultOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. +This can be useful when you need to supply an `OAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. + +The following code shows an example of the `contextAttributesMapper`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, + // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); + + return authorizedClientManager; +} + +private Function> contextAttributesMapper() { + return authorizeRequest -> { + Map contextAttributes = Collections.emptyMap(); + HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); + String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME); + String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = new HashMap<>(); + + // `PasswordOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + return contextAttributes; + }; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, + // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name) + val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME) + val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password + } + contextAttributes + } +} +---- +==== + +The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`. +When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead. + +A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`. +Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. +An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. + +The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService authorizedClientService) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc new file mode 100644 index 00000000000..e16d3f4ef77 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc @@ -0,0 +1,149 @@ +[[oauth2client]] += OAuth 2.0 Client +:page-section-summary-toc: 1 + +The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. + +At a high-level, the core features available are: + +.Authorization Grant support +* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] +* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] +* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] +* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] + +.Client Authentication support +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] + +.HTTP Client support +* xref:servlet/oauth2/client/authorized-clients.adoc#oauth2Client-webclient-servlet[`WebClient` integration for Servlet Environments] (for requesting protected resources) + +The `HttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. +In addition, `HttpSecurity.oauth2Client().authorizationCodeGrant()` enables the customization of the Authorization Code grant. + +The following code shows the complete configuration options provided by the `HttpSecurity.oauth2Client()` DSL: + +.OAuth2 Client Configuration Options +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class OAuth2ClientSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .oauth2Client(oauth2 -> oauth2 + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizedClientService(this.authorizedClientService()) + .authorizationCodeGrant(codeGrant -> codeGrant + .authorizationRequestRepository(this.authorizationRequestRepository()) + .authorizationRequestResolver(this.authorizationRequestResolver()) + .accessTokenResponseClient(this.accessTokenResponseClient()) + ) + ); + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class OAuth2ClientSecurityConfig { + + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + oauth2Client { + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizedClientService = authorizedClientService() + authorizationCodeGrant { + authorizationRequestRepository = authorizationRequestRepository() + authorizationRequestResolver = authorizationRequestResolver() + accessTokenResponseClient = accessTokenResponseClient() + } + } + } + return http.build() + } +} +---- +==== + +In addition to the `HttpSecurity.oauth2Client()` DSL, XML configuration is also supported. + +The following code shows the complete configuration options available in the xref:servlet/appendix/namespace/http.adoc#nsa-oauth2-client[ security namespace]: + +.OAuth2 Client XML Configuration Options +==== +[source,xml] +---- + + + + + +---- +==== + +The `OAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `OAuth2AuthorizedClientProvider`(s). + +The following code shows an example of how to register an `OAuth2AuthorizedClientManager` `@Bean` and associate it with an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider: OAuth2AuthorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc similarity index 52% rename from docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc rename to docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc index db5ee9c5065..1d0009f0856 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc @@ -1,581 +1,5 @@ -[[oauth2login]] -= OAuth 2.0 Login - -The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). -OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". - -NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. - - -[[oauth2login-sample-boot]] -== Spring Boot 2.x Sample - -Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. - -This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: - -* <> -* <> -* <> -* <> - - -[[oauth2login-sample-initial-setup]] -=== Initial setup - -To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. - -NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. - -Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". - -After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. - - -[[oauth2login-sample-redirect-uri]] -=== Setting the redirect URI - -The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. - -In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. - -TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]. - -IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. - - -[[oauth2login-sample-application-config]] -=== Configure application.yml - -Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. -To do so: - -. Go to `application.yml` and set the following configuration: -+ -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: <1> - google: <2> - client-id: google-client-id - client-secret: google-client-secret ----- -+ -.OAuth Client properties -==== -<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration], such as google. -==== - -. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. - - -[[oauth2login-sample-boot-application]] -=== Boot up the application - -Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. -You are then redirected to the default _auto-generated_ login page, which displays a link for Google. - -Click on the Google link, and you are then redirected to Google for authentication. - -After authenticating with your Google account credentials, the next page presented to you is the Consent screen. -The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. -Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. - -At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. - - -[[oauth2login-boot-property-mappings]] -== Spring Boot 2.x Property Mappings - -The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration] properties. - -|=== -|Spring Boot 2.x |ClientRegistration - -|`spring.security.oauth2.client.registration._[registrationId]_` -|`registrationId` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-id` -|`clientId` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` -|`clientSecret` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` -|`clientAuthenticationMethod` - -|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` -|`authorizationGrantType` - -|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` -|`redirectUri` - -|`spring.security.oauth2.client.registration._[registrationId]_.scope` -|`scopes` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-name` -|`clientName` - -|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` -|`providerDetails.authorizationUri` - -|`spring.security.oauth2.client.provider._[providerId]_.token-uri` -|`providerDetails.tokenUri` - -|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` -|`providerDetails.jwkSetUri` - -|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` -|`providerDetails.issuerUri` - -|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` -|`providerDetails.userInfoEndpoint.uri` - -|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` -|`providerDetails.userInfoEndpoint.authenticationMethod` - -|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` -|`providerDetails.userInfoEndpoint.userNameAttributeName` -|=== - -[TIP] -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. - - -[[oauth2login-common-oauth2-provider]] -== CommonOAuth2Provider - -`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. - -For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. -Therefore, it makes sense to provide default values in order to reduce the required configuration. - -As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - google: - client-id: google-client-id - client-secret: google-client-secret ----- - -[TIP] -The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. - -For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - google-login: <1> - provider: google <2> - client-id: google-client-id - client-secret: google-client-secret ----- -<1> The `registrationId` is set to `google-login`. -<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. - - -[[oauth2login-custom-provider-properties]] -== Configuring Custom Provider Properties - -There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). - -For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. - -For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - provider: - okta: <1> - authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize - token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token - user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo - user-name-attribute: sub - jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys ----- - -<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. - - -[[oauth2login-override-boot-autoconfig]] -== Overriding Spring Boot 2.x Auto-configuration - -The Spring Boot 2.x auto-configuration class for OAuth Client support is `OAuth2ClientAutoConfiguration`. - -It performs the following tasks: - -* Registers a `ClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. -* Provides a `WebSecurityConfigurerAdapter` `@Configuration` and enables OAuth 2.0 Login through `httpSecurity.oauth2Login()`. - -If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: - -* <> -* <> -* <> - - -[[oauth2login-register-clientregistrationrepository-bean]] -=== Register a ClientRegistrationRepository @Bean - -The following example shows how to register a `ClientRegistrationRepository` `@Bean`: - -==== -.Java -[source,java,role="primary",attrs="-attributes"] ----- -@Configuration -public class OAuth2LoginConfig { - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - private ClientRegistration googleClientRegistration() { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary",attrs="-attributes"] ----- -@Configuration -class OAuth2LoginConfig { - @Bean - fun clientRegistrationRepository(): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(googleClientRegistration()) - } - - private fun googleClientRegistration(): ClientRegistration { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build() - } -} ----- -==== - - -[[oauth2login-provide-websecurityconfigureradapter]] -=== Provide a WebSecurityConfigurerAdapter - -The following example shows how to provide a `WebSecurityConfigurerAdapter` with `@EnableWebSecurity` and enable OAuth 2.0 login through `httpSecurity.oauth2Login()`: - -.OAuth2 Login Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - } - } -} ----- -==== - - -[[oauth2login-completely-override-autoconfiguration]] -=== Completely Override the Auto-configuration - -The following example shows how to completely override the auto-configuration by registering a `ClientRegistrationRepository` `@Bean` and providing a `WebSecurityConfigurerAdapter`. - -.Overriding the auto-configuration -==== -.Java -[source,java,role="primary",attrs="-attributes"] ----- -@Configuration -public class OAuth2LoginConfig { - - @EnableWebSecurity - public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()); - } - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - private ClientRegistration googleClientRegistration() { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary",attrs="-attributes"] ----- -@Configuration -class OAuth2LoginConfig { - - @EnableWebSecurity - class OAuth2LoginSecurityConfig: WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - } - } - } - - @Bean - fun clientRegistrationRepository(): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(googleClientRegistration()) - } - - private fun googleClientRegistration(): ClientRegistration { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build() - } -} ----- -==== - - -[[oauth2login-javaconfig-wo-boot]] -== Java Configuration without Spring Boot 2.x - -If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: - -.OAuth2 Login Configuration -==== -.Java -[source,java,role="primary"] ----- -@Configuration -public class OAuth2LoginConfig { - - @EnableWebSecurity - public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()); - } - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - @Bean - public OAuth2AuthorizedClientService authorizedClientService( - ClientRegistrationRepository clientRegistrationRepository) { - return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); - } - - @Bean - public OAuth2AuthorizedClientRepository authorizedClientRepository( - OAuth2AuthorizedClientService authorizedClientService) { - return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); - } - - private ClientRegistration googleClientRegistration() { - return CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Configuration -open class OAuth2LoginConfig { - @EnableWebSecurity - open class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - } - } - } - - @Bean - open fun clientRegistrationRepository(): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(googleClientRegistration()) - } - - @Bean - open fun authorizedClientService( - clientRegistrationRepository: ClientRegistrationRepository? - ): OAuth2AuthorizedClientService { - return InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository) - } - - @Bean - open fun authorizedClientRepository( - authorizedClientService: OAuth2AuthorizedClientService? - ): OAuth2AuthorizedClientRepository { - return AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService) - } - - private fun googleClientRegistration(): ClientRegistration { - return CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .build() - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - - - - - - - - ----- -==== - - [[oauth2login-advanced]] -== Advanced Configuration += Advanced Configuration `HttpSecurity.oauth2Login()` provides a number of configuration options for customizing OAuth 2.0 Login. The main configuration options are grouped into their protocol endpoint counterparts. @@ -590,10 +14,10 @@ The following code shows an example: [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .authorizationEndpoint(authorization -> authorization @@ -609,6 +33,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } } ---- @@ -617,9 +42,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { authorizationEndpoint { @@ -636,6 +62,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -668,10 +95,10 @@ The following code shows the complete configuration options available for the `o [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .clientRegistrationRepository(this.clientRegistrationRepository()) @@ -695,6 +122,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { .oidcUserService(this.oidcUserService()) ) ); + return http.build(); } } ---- @@ -703,9 +131,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { clientRegistrationRepository = clientRegistrationRepository() @@ -730,6 +159,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -737,7 +167,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { In addition to the `oauth2Login()` DSL, XML configuration is also supported. -The following code shows the complete configuration options available in the xref:servlet/appendix/namespace.adoc#nsa-oauth2-login[ security namespace]: +The following code shows the complete configuration options available in the xref:servlet/appendix/namespace/http.adoc#nsa-oauth2-login[ security namespace]: .OAuth2 Login XML Configuration Options ==== @@ -767,10 +197,12 @@ The following sections go into more detail on each of the configuration options * <> * <> * <> +* <> +* <> [[oauth2login-advanced-login-page]] -=== OAuth 2.0 Login Page +== OAuth 2.0 Login Page By default, the OAuth 2.0 Login Page is auto-generated by the `DefaultLoginPageGeneratingFilter`. The default login page shows each configured OAuth Client with its `ClientRegistration.clientName` as a link, which is capable of initiating the Authorization Request (or OAuth 2.0 Login). @@ -800,10 +232,10 @@ The following listing shows an example: [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .loginPage("/login/oauth2") @@ -813,6 +245,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } } ---- @@ -821,9 +254,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { loginPage = "/login/oauth2" @@ -832,6 +266,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -865,7 +300,7 @@ The following line shows an example: [[oauth2login-advanced-redirection-endpoint]] -=== Redirection Endpoint +== Redirection Endpoint The Redirection Endpoint is used by the Authorization Server for returning the Authorization Response (which contains the authorization credentials) to the client via the Resource Owner user-agent. @@ -883,10 +318,10 @@ If you would like to customize the Authorization Response `baseUri`, configure i [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .redirectionEndpoint(redirection -> redirection @@ -894,6 +329,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } } ---- @@ -902,9 +338,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { redirectionEndpoint { @@ -912,6 +349,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -956,7 +394,7 @@ return CommonOAuth2Provider.GOOGLE.getBuilder("google") [[oauth2login-advanced-userinfo-endpoint]] -=== UserInfo Endpoint +== UserInfo Endpoint The UserInfo Endpoint includes a number of configuration options, as described in the following sub-sections: @@ -966,7 +404,7 @@ The UserInfo Endpoint includes a number of configuration options, as described i [[oauth2login-advanced-map-authorities]] -==== Mapping User Authorities +=== Mapping User Authorities After the user successfully authenticates with the OAuth 2.0 Provider, the `OAuth2User.getAuthorities()` (or `OidcUser.getAuthorities()`) may be mapped to a new set of `GrantedAuthority` instances, which will be supplied to `OAuth2AuthenticationToken` when completing the authentication. @@ -980,7 +418,7 @@ There are a couple of options to choose from when mapping user authorities: [[oauth2login-advanced-map-authorities-grantedauthoritiesmapper]] -===== Using a GrantedAuthoritiesMapper +==== Using a GrantedAuthoritiesMapper Provide an implementation of `GrantedAuthoritiesMapper` and configure it as shown in the following example: @@ -990,10 +428,10 @@ Provide an implementation of `GrantedAuthoritiesMapper` and configure it as show [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo @@ -1001,6 +439,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } private GrantedAuthoritiesMapper userAuthoritiesMapper() { @@ -1038,9 +477,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { userInfoEndpoint { @@ -1048,6 +488,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } private fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection -> @@ -1090,12 +531,13 @@ Alternatively, you may register a `GrantedAuthoritiesMapper` `@Bean` to have it [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(withDefaults()); + return http.build(); } @Bean @@ -1109,12 +551,14 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { } } + return http.build() } @Bean @@ -1126,7 +570,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { ==== [[oauth2login-advanced-map-authorities-oauth2userservice]] -===== Delegation-based strategy with OAuth2UserService +==== Delegation-based strategy with OAuth2UserService This strategy is advanced compared to using a `GrantedAuthoritiesMapper`, however, it's also more flexible as it gives you access to the `OAuth2UserRequest` and `OAuth2User` (when using an OAuth 2.0 UserService) or `OidcUserRequest` and `OidcUser` (when using an OpenID Connect 1.0 UserService). @@ -1140,10 +584,10 @@ The following example shows how to implement and configure a delegation-based st [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo @@ -1151,6 +595,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } private OAuth2UserService oidcUserService() { @@ -1180,9 +625,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { userInfoEndpoint { @@ -1190,6 +636,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } @Bean @@ -1228,7 +675,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2login-advanced-oauth2-user-service]] -==== OAuth 2.0 UserService +=== OAuth 2.0 UserService `DefaultOAuth2UserService` is an implementation of an `OAuth2UserService` that supports standard OAuth 2.0 Provider's. @@ -1259,10 +706,10 @@ Whether you customize `DefaultOAuth2UserService` or provide your own implementat [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo @@ -1270,6 +717,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } private OAuth2UserService oauth2UserService() { @@ -1282,9 +730,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { userInfoEndpoint { @@ -1293,6 +742,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } private fun oauth2UserService(): OAuth2UserService { @@ -1304,7 +754,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2login-advanced-oidc-user-service]] -==== OpenID Connect 1.0 UserService +=== OpenID Connect 1.0 UserService `OidcUserService` is an implementation of an `OAuth2UserService` that supports OpenID Connect 1.0 Provider's. @@ -1319,10 +769,10 @@ Whether you customize `OidcUserService` or provide your own implementation of `O [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo @@ -1330,6 +780,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { ... ) ); + return http.build(); } private OAuth2UserService oidcUserService() { @@ -1342,9 +793,10 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { oauth2Login { userInfoEndpoint { @@ -1353,6 +805,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { } } } + return http.build() } private fun oidcUserService(): OAuth2UserService { @@ -1364,7 +817,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2login-advanced-idtoken-verify]] -=== ID Token Signature Verification +== ID Token Signature Verification OpenID Connect 1.0 Authentication introduces the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token], which is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when used by a Client. @@ -1409,7 +862,7 @@ If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authe [[oauth2login-advanced-oidc-logout]] -=== OpenID Connect 1.0 Logout +== OpenID Connect 1.0 Logout OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. One of the strategies available is https://openid.net/specs/openid-connect-session-1_0.html#RPLogout[RP-Initiated Logout]. @@ -1440,21 +893,22 @@ spring: [source,java,role="primary"] ---- @EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { +public class OAuth2LoginSecurityConfig { @Autowired private ClientRegistrationRepository clientRegistrationRepository; - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(withDefaults()) .logout(logout -> logout .logoutSuccessHandler(oidcLogoutSuccessHandler()) ); + return http.build(); } private LogoutSuccessHandler oidcLogoutSuccessHandler() { @@ -1468,20 +922,18 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { return oidcLogoutSuccessHandler; } } - -NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder. -If used, the application's base URL, like `https://app.example.org`, will replace it at request time. ---- .Kotlin [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { +class OAuth2LoginSecurityConfig { @Autowired private lateinit var clientRegistrationRepository: ClientRegistrationRepository - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -1491,6 +943,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { logoutSuccessHandler = oidcLogoutSuccessHandler() } } + return http.build() } private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler { @@ -1502,8 +955,8 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { return oidcLogoutSuccessHandler } } +---- +==== NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder. If used, the application's base URL, like `https://app.example.org`, will replace it at request time. ----- -==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc new file mode 100644 index 00000000000..8d19b5d73d5 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc @@ -0,0 +1,560 @@ += Core Configuration + +[[oauth2login-sample-boot]] +== Spring Boot 2.x Sample + +Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. + +This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: + +* <> +* <> +* <> +* <> + + +[[oauth2login-sample-initial-setup]] +=== Initial setup + +To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. + +NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. + +Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". + +After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. + + +[[oauth2login-sample-redirect-uri]] +=== Setting the redirect URI + +The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. + +In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. + +TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. +The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration]. + +IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. +Also, see the supported xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. + + +[[oauth2login-sample-application-config]] +=== Configure application.yml + +Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. +To do so: + +. Go to `application.yml` and set the following configuration: ++ +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: <1> + google: <2> + client-id: google-client-id + client-secret: google-client-secret +---- ++ +.OAuth Client properties +==== +<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. +<2> Following the base property prefix is the ID for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration], such as google. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. + + +[[oauth2login-sample-boot-application]] +=== Boot up the application + +Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. +You are then redirected to the default _auto-generated_ login page, which displays a link for Google. + +Click on the Google link, and you are then redirected to Google for authentication. + +After authenticating with your Google account credentials, the next page presented to you is the Consent screen. +The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. +Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. + +At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. + + +[[oauth2login-boot-property-mappings]] +== Spring Boot 2.x Property Mappings + +The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration] properties. + +|=== +|Spring Boot 2.x |ClientRegistration + +|`spring.security.oauth2.client.registration._[registrationId]_` +|`registrationId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-id` +|`clientId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` +|`clientSecret` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` +|`clientAuthenticationMethod` + +|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` +|`authorizationGrantType` + +|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` +|`redirectUri` + +|`spring.security.oauth2.client.registration._[registrationId]_.scope` +|`scopes` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-name` +|`clientName` + +|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` +|`providerDetails.authorizationUri` + +|`spring.security.oauth2.client.provider._[providerId]_.token-uri` +|`providerDetails.tokenUri` + +|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` +|`providerDetails.jwkSetUri` + +|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` +|`providerDetails.issuerUri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` +|`providerDetails.userInfoEndpoint.uri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` +|`providerDetails.userInfoEndpoint.authenticationMethod` + +|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` +|`providerDetails.userInfoEndpoint.userNameAttributeName` +|=== + +[TIP] +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. + + +[[oauth2login-common-oauth2-provider]] +== CommonOAuth2Provider + +`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. + +For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. +Therefore, it makes sense to provide default values in order to reduce the required configuration. + +As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google: + client-id: google-client-id + client-secret: google-client-secret +---- + +[TIP] +The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. + +For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google-login: <1> + provider: google <2> + client-id: google-client-id + client-secret: google-client-secret +---- +<1> The `registrationId` is set to `google-login`. +<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. + + +[[oauth2login-custom-provider-properties]] +== Configuring Custom Provider Properties + +There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). + +For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. + +For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + provider: + okta: <1> + authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize + token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token + user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo + user-name-attribute: sub + jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys +---- + +<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. + + +[[oauth2login-override-boot-autoconfig]] +== Overriding Spring Boot 2.x Auto-configuration + +The Spring Boot 2.x auto-configuration class for OAuth Client support is `OAuth2ClientAutoConfiguration`. + +It performs the following tasks: + +* Registers a `ClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. +* Registers a `SecurityFilterChain` `@Bean` and enables OAuth 2.0 Login through `httpSecurity.oauth2Login()`. + +If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: + +* <> +* <> +* <> + + +[[oauth2login-register-clientregistrationrepository-bean]] +=== Register a ClientRegistrationRepository @Bean + +The following example shows how to register a `ClientRegistrationRepository` `@Bean`: + +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@Configuration +public class OAuth2LoginConfig { + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@Configuration +class OAuth2LoginConfig { + @Bean + fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[oauth2login-provide-securityfilterchain-bean]] +=== Register a SecurityFilterChain @Bean + +The following example shows how to register a `SecurityFilterChain` `@Bean` with `@EnableWebSecurity` and enable OAuth 2.0 login through `httpSecurity.oauth2Login()`: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()); + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class OAuth2LoginSecurityConfig { + + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + } + return http.build() + } +} +---- +==== + + +[[oauth2login-completely-override-autoconfiguration]] +=== Completely Override the Auto-configuration + +The following example shows how to completely override the auto-configuration by registering a `ClientRegistrationRepository` `@Bean` and a `SecurityFilterChain` `@Bean`. + +.Overriding the auto-configuration +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@Configuration +public class OAuth2LoginConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()); + return http.build(); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@Configuration +class OAuth2LoginConfig { + + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + } + return http.build() + } + + @Bean + fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[oauth2login-javaconfig-wo-boot]] +== Java Configuration without Spring Boot 2.x + +If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()); + return http.build(); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + @Bean + public OAuth2AuthorizedClientService authorizedClientService( + ClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository( + OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } + + private ClientRegistration googleClientRegistration() { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +open class OAuth2LoginConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + } + return http.build() + } + + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + @Bean + open fun authorizedClientService( + clientRegistrationRepository: ClientRegistrationRepository? + ): OAuth2AuthorizedClientService { + return InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository) + } + + @Bean + open fun authorizedClientRepository( + authorizedClientService: OAuth2AuthorizedClientService? + ): OAuth2AuthorizedClientRepository { + return AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService) + } + + private fun googleClientRegistration(): ClientRegistration { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build() + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + + + + + + + + +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc new file mode 100644 index 00000000000..13adc137e5c --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc @@ -0,0 +1,8 @@ +[[oauth2login]] += OAuth 2.0 Login +:page-section-summary-toc: 1 + +The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). +OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". + +NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc index 9c35a4b87db..4365c9e6e22 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc @@ -96,7 +96,7 @@ http { == Bearer Token Propagation -Now that you're resource server has validated the token, it might be handy to pass it to downstream services. +Now that your resource server has validated the token, it might be handy to pass it to downstream services. This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.html[ServletBearerExchangeFilterFunction]`, which you can see in the following example: ==== @@ -241,7 +241,7 @@ fun rest(): RestTemplate { [NOTE] Unlike the {security-api-url}org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.html[OAuth 2.0 Authorized Client Manager], this filter interceptor makes no attempt to renew the token, should it be expired. -To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. +To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/client/index.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. [[oauth2resourceserver-bearertoken-failure]] == Bearer Token Failure diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc index 64ebfda12f1..8219292fe5b 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -137,26 +137,29 @@ This property can also be supplied directly on the < authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + return http.build(); } ---- .Kotlin [source,kotlin,role="secondary"] ---- -fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -165,11 +168,12 @@ fun configure(http: HttpSecurity) { jwt { } } } + return http.build() } ---- ==== -If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. +If the application doesn't expose a `SecurityFilterChain` bean, then Spring Boot will expose the above default one. Replacing this is as simple as exposing the bean within the application: @@ -179,10 +183,11 @@ Replacing this is as simple as exposing the bean within the application: [source,java,role="primary"] ---- @EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class MyCustomSecurityConfiguration { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) @@ -191,6 +196,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter .jwtAuthenticationConverter(myConverter()) ) ); + return http.build(); } } ---- @@ -199,8 +205,9 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class MyCustomSecurityConfiguration { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/messages/**", hasAuthority("SCOPE_message:read")) @@ -212,6 +219,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -296,10 +304,11 @@ An authorization server's JWK Set Uri can be configured < authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -307,6 +316,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { .jwkSetUri("https://idp.example.com/.well-known/jwks.json") ) ); + return http.build(); } } ---- @@ -315,8 +325,9 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class DirectlyConfiguredJwkSetUri { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -327,6 +338,7 @@ class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -356,10 +368,11 @@ More powerful than `jwkSetUri()` is `decoder()`, which will completely replace a [source,java,role="primary"] ---- @EnableWebSecurity -public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class DirectlyConfiguredJwtDecoder { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -367,6 +380,7 @@ public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { .decoder(myCustomDecoder()) ) ); + return http.build(); } } ---- @@ -375,8 +389,9 @@ public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class DirectlyConfiguredJwtDecoder { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -387,6 +402,7 @@ class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -716,15 +732,17 @@ This means that to protect an endpoint or method with a scope derived from a JWT [source,java,role="primary"] ---- @EnableWebSecurity -public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class DirectlyConfiguredJwkSetUri { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + return http.build(); } } ---- @@ -733,8 +751,9 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class DirectlyConfiguredJwkSetUri { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/contacts/**", hasAuthority("SCOPE_contacts")) @@ -745,6 +764,7 @@ class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { jwt { } } } + return http.build() } } ---- @@ -923,10 +943,11 @@ static class CustomAuthenticationConverter implements Converter authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -934,6 +955,7 @@ public class CustomAuthenticationConverterConfig extends WebSecurityConfigurerAd .jwtAuthenticationConverter(new CustomAuthenticationConverter()) ) ); + return http.build(); } } ---- @@ -950,8 +972,9 @@ internal class CustomAuthenticationConverter : Converter authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -100,7 +100,7 @@ In each case, there are two things that need to be done and trade-offs associate One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerAuthenticationManagerResolver`, like so: -.Multitenancy Tenant by JWT Claim +.Multi-tenancy Tenant by JWT Claim ==== .Java [source,java,role="primary"] @@ -109,7 +109,7 @@ JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIs ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -176,7 +176,7 @@ JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get); http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -323,7 +323,7 @@ Next, we can construct a `JWTProcessor`: JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) { ConfigurableJWTProcessor 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 jwtValidator) { - NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor); + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<> - (JwtValidators.createDefault(), this.jwtValidator); + (JwtValidators.createDefault(), jwtValidator); decoder.setJwtValidator(validator); return decoder; } diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index 4e0c618599c..ae711c84b72 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -24,13 +24,14 @@ To specify where the introspection endpoint is, simply do: [source,yaml] ---- -security: - oauth2: - resourceserver: - opaque-token: - introspection-uri: https://idp.example.com/introspect - client-id: client - client-secret: secret +spring: + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://idp.example.com/introspect + client-id: client + client-secret: secret ---- Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint. @@ -178,27 +179,30 @@ fun forFoosEyesOnly(): String { There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf. -The first is a `WebSecurityConfigurerAdapter` that configures the app as a resource server. -When use Opaque Token, this `WebSecurityConfigurerAdapter` looks like: +The first is a `SecurityFilterChain` that configures the app as a resource server. +When use Opaque Token, this `SecurityFilterChain` looks like: .Default Opaque Token Configuration ==== .Java [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); } ---- .Kotlin [source,kotlin,role="secondary"] ---- -override fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -207,11 +211,12 @@ override fun configure(http: HttpSecurity) { opaqueToken { } } } + return http.build() } ---- ==== -If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. +If the application doesn't expose a `SecurityFilterChain` bean, then Spring Boot will expose the above default one. Replacing this is as simple as exposing the bean within the application: @@ -221,10 +226,11 @@ Replacing this is as simple as exposing the bean within the application: [source,java,role="primary"] ---- @EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class MyCustomSecurityConfiguration { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) @@ -233,6 +239,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter .introspector(myIntrospector()) ) ); + return http.build(); } } ---- @@ -241,8 +248,9 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class MyCustomSecurityConfiguration { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/messages/**", hasAuthority("SCOPE_message:read")) @@ -254,6 +262,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -335,10 +344,11 @@ An authorization server's Introspection Uri can be configured < authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -347,6 +357,7 @@ public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAda .introspectionClientCredentials("client", "secret") ) ); + return http.build(); } } ---- @@ -355,8 +366,9 @@ public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAda [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class DirectlyConfiguredIntrospectionUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class DirectlyConfiguredIntrospectionUri { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -368,6 +380,7 @@ class DirectlyConfiguredIntrospectionUri : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -397,10 +410,11 @@ More powerful than `introspectionUri()` is `introspector()`, which will complete [source,java,role="primary"] ---- @EnableWebSecurity -public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class DirectlyConfiguredIntrospector { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -408,6 +422,7 @@ public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter .introspector(myCustomIntrospector()) ) ); + return http.build(); } } ---- @@ -416,8 +431,9 @@ public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class DirectlyConfiguredIntrospector : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class DirectlyConfiguredIntrospector { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) @@ -428,6 +444,7 @@ class DirectlyConfiguredIntrospector : WebSecurityConfigurerAdapter() { } } } + return http.build() } } ---- @@ -476,15 +493,17 @@ This means that to protect an endpoint or method with a scope derived from an Op [source,java,role="primary"] ---- @EnableWebSecurity -public class MappedAuthorities extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class MappedAuthorities { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorizeRequests -> authorizeRequests + .authorizeHttpRequests(authorizeRequests -> authorizeRequests .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); } } ---- @@ -493,8 +512,9 @@ public class MappedAuthorities extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class MappedAuthorities : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class MappedAuthorities { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/contacts/**", hasAuthority("SCOPE_contacts")) @@ -505,6 +525,7 @@ class MappedAuthorities : WebSecurityConfigurerAdapter() { opaqueToken { } } } + return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index ba394250a43..c8a09a67374 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -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 ``. @@ -176,92 +176,21 @@ var relyingPartyRegistration: RelyingPartyRegistration? = There are a number of reasons that you may want to adjust an `AuthnRequest`. For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default. -If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to xref:servlet/saml2/login/overview.adoc#servlet-saml2login-opensaml-customization[register a custom `AuthnRequestMarshaller` with OpenSAML]. -This will give you access to post-process the `AuthnRequest` instance before it's serialized. - -But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter` to build an `AuthnRequest` yourself, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Component -public class AuthnRequestConverter implements - Converter { - - private final AuthnRequestBuilder authnRequestBuilder; - private final IssuerBuilder issuerBuilder; - - // ... constructor - - public AuthnRequest convert(Saml2AuthenticationRequestContext context) { - MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; - Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(myContext.getIssuer()); - - AuthnRequest authnRequest = authnRequestBuilder.buildObject(); - authnRequest.setIssuer(issuer); - authnRequest.setDestination(myContext.getDestination()); - authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); - - // ... additional settings - - authRequest.setForceAuthn(myContext.getForceAuthn()); - return authnRequest; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Component -class AuthnRequestConverter : Converter { - private val authnRequestBuilder: AuthnRequestBuilder? = null - private val issuerBuilder: IssuerBuilder? = null - - // ... constructor - override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest { - val myContext: MySaml2AuthenticationRequestContext = context - val issuer: Issuer = issuerBuilder.buildObject() - issuer.value = myContext.getIssuer() - val authnRequest: AuthnRequest = authnRequestBuilder.buildObject() - authnRequest.issuer = issuer - authnRequest.destination = myContext.getDestination() - authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl() - - // ... additional settings - authRequest.setForceAuthn(myContext.getForceAuthn()) - return authnRequest - } -} ----- -==== - -Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as ``@Bean``s: +You can customize elements of OpenSAML's `AuthnRequest` by publishing an `OpenSaml4AuthenticationRequestResolver` as a `@Bean`, like so: ==== .Java [source,java,role="primary"] ---- @Bean -Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() { - Saml2AuthenticationRequestContextResolver resolver = - new DefaultSaml2AuthenticationRequestContextResolver(); - return request -> { - Saml2AuthenticationRequestContext context = resolver.resolve(request); - return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); - }; -} - -@Bean -Saml2AuthenticationRequestFactory authenticationRequestFactory( - AuthnRequestConverter authnRequestConverter) { - - OpenSaml4AuthenticationRequestFactory authenticationRequestFactory = - new OpenSaml4AuthenticationRequestFactory(); - authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter); - return authenticationRequestFactory; +Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver registrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations); + OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = + new OpenSaml4AuthenticationRequestResolver(registrationResolver); + authenticationRequestResolver.setAuthnRequestCustomizer((context) -> context + .getAuthnRequest().setForceAuthn(true)); + return authenticationRequestResolver; } ---- @@ -269,24 +198,14 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory( [source,kotlin,role="secondary"] ---- @Bean -open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { - val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() - return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> - val context = resolver.resolve(request) - MySaml2AuthenticationRequestContext( - context, - request.getParameter("force") != null - ) - } -} - -@Bean -open fun authenticationRequestFactory( - authnRequestConverter: AuthnRequestConverter? -): Saml2AuthenticationRequestFactory? { - val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory() - authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter) - return authenticationRequestFactory +fun authenticationRequestResolver(registrations : RelyingPartyRegistrationRepository) : Saml2AuthenticationRequestResolver { + val registrationResolver : RelyingPartyRegistrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations) + val authenticationRequestResolver : OpenSaml4AuthenticationRequestResolver = + new OpenSaml4AuthenticationRequestResolver(registrationResolver) + authenticationRequestResolver.setAuthnRequestCustomizer((context) -> context + .getAuthnRequest().setForceAuthn(true)) + return authenticationRequestResolver } ---- ==== diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index 2d6efa7ab2c..d4adad3bc62 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -23,10 +23,10 @@ For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default [source,java,role="primary"] ---- @EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { +public class SecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider .createDefaultAssertionValidator(assertionToken -> { @@ -38,12 +38,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { ); http - .authorizeRequests(authz -> authz + .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 .authenticationManager(new ProviderManager(authenticationProvider)) ); + return http.build(); } } ---- @@ -52,8 +53,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -open class SecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +open class SecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { val authenticationProvider = OpenSaml4AuthenticationProvider() authenticationProvider.setAssertionValidator( OpenSaml4AuthenticationProvider @@ -72,6 +74,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() { authenticationManager = ProviderManager(authenticationProvider) } } + return http.build() } } ---- @@ -88,12 +91,12 @@ In that case, the response authentication converter can come in handy, as can be [source,java,role="primary"] ---- @EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { +public class SecurityConfig { @Autowired UserDetailsService userDetailsService; - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); authenticationProvider.setResponseAuthenticationConverter(responseToken -> { Saml2Authentication authentication = OpenSaml4AuthenticationProvider @@ -106,12 +109,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { }); http - .authorizeRequests(authz -> authz + .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 .authenticationManager(new ProviderManager(authenticationProvider)) ); + return http.build(); } } ---- @@ -120,11 +124,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -open class SecurityConfig : WebSecurityConfigurerAdapter() { +open class SecurityConfig { @Autowired var userDetailsService: UserDetailsService? = null - override fun configure(http: HttpSecurity) { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { val authenticationProvider = OpenSaml4AuthenticationProvider() authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken -> val authentication = OpenSaml4AuthenticationProvider @@ -143,6 +148,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() { authenticationManager = ProviderManager(authenticationProvider) } } + return http.build() } } ---- @@ -304,19 +310,20 @@ This authentication manager should expect a `Saml2AuthenticationToken` object co [source,java,role="primary"] ---- @EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { +public class SecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...); http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 .authenticationManager(authenticationManager) ) ; + return http.build(); } } ---- @@ -325,8 +332,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { [source,kotlin,role="secondary"] ---- @EnableWebSecurity -open class SecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +open class SecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...) http { authorizeRequests { @@ -336,6 +344,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() { authenticationManager = customAuthenticationManager } } + return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc index 30b8a715e69..fd973b0a839 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc @@ -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]. ==== diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index e7198c64f9c..a527f0a9252 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -3,7 +3,7 @@ :icondir: icons Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security. -First, we see that, like xref:servlet/oauth2/oauth2-login.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication. +First, we see that, like xref:servlet/oauth2/login/index.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication. It does this through a series of redirects. .Redirecting to Asserting Party Authentication @@ -279,38 +279,42 @@ The `requireInitialize` method may only be called once per application instance. There are two ``@Bean``s that Spring Boot generates for a relying party. -The first is a `WebSecurityConfigurerAdapter` that configures the app as a relying party. -When including `spring-security-saml2-service-provider`, the `WebSecurityConfigurerAdapter` looks like: +The first is a `SecurityFilterChain` that configures the app as a relying party. +When including `spring-security-saml2-service-provider`, the `SecurityFilterChain` looks like: .Default JWT Configuration ==== .Java [source,java,role="primary"] ---- -protected void configure(HttpSecurity http) { +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .saml2Login(withDefaults()); + return http.build(); } ---- .Kotlin [source,kotlin,role="secondary"] ---- -fun configure(http: HttpSecurity) { +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) } saml2Login { } } + return http.build() } ---- ==== -If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. +If the application doesn't expose a `SecurityFilterChain` bean, then Spring Boot will expose the above default one. You can replace this by exposing the bean within the application: @@ -320,14 +324,16 @@ You can replace this by exposing the bean within the application: [source,java,role="primary"] ---- @EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class MyCustomSecurityConfiguration { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("ROLE_USER") .anyRequest().authenticated() ) .saml2Login(withDefaults()); + return http.build(); } } ---- @@ -336,8 +342,9 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class MyCustomSecurityConfiguration { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/messages/**", hasAuthority("ROLE_USER")) @@ -346,6 +353,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { saml2Login { } } + return http.build() } } ---- @@ -460,7 +468,7 @@ Note that `X509Support` is an OpenSAML class, used here in the snippet for brevi [[servlet-saml2login-relyingpartyregistrationrepository-dsl]] -Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `WebSecurityConfigurerAdapter`: +Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `SecurityFilterChain`: .Custom Relying Party Registration DSL ==== @@ -468,16 +476,18 @@ Alternatively, you can directly wire up the repository using the DSL, which will [source,java,role="primary"] ---- @EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { +public class MyCustomSecurityConfiguration { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("ROLE_USER") .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 .relyingPartyRegistrationRepository(relyingPartyRegistrations()) ); + return http.build(); } } ---- @@ -486,8 +496,9 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter [source,kotlin,role="secondary"] ---- @EnableWebSecurity -class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { +class MyCustomSecurityConfiguration { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { authorize("/messages/**", hasAuthority("ROLE_USER")) @@ -497,6 +508,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { relyingPartyRegistrationRepository = relyingPartyRegistrations() } } + return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 0d1a886753f..9dba271b78c 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -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); @@ -43,7 +44,7 @@ RelyingPartyRegistrationRepository registrations() { @Bean SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { http - .authorizeRequests((authorize) -> authorize + .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) .saml2Login(withDefaults()) @@ -73,6 +74,10 @@ Also, your application can participate in an AP-initiated logout when the assert 3. Create, sign, and serialize a `` 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: diff --git a/docs/modules/ROOT/pages/servlet/test/method.adoc b/docs/modules/ROOT/pages/servlet/test/method.adoc index e5e639464e7..ba34339f9ec 100644 --- a/docs/modules/ROOT/pages/servlet/test/method.adoc +++ b/docs/modules/ROOT/pages/servlet/test/method.adoc @@ -49,7 +49,7 @@ Before we can use Spring Security Test support, we must perform some setup. An e .Java [source,java,role="primary"] ---- -@RunWith(SpringJUnit4ClassRunner.class) // <1> +@ExtendWith(SpringExtension.class) // <1> @ContextConfiguration // <2> public class WithMockUserTests { ---- @@ -57,16 +57,15 @@ public class WithMockUserTests { .Kotlin [source,kotlin,role="secondary"] ---- -@RunWith(SpringJUnit4ClassRunner::class) +@ExtendWith(SpringExtension.class) @ContextConfiguration class WithMockUserTests { ---- -==== This is a basic example of how to setup Spring Security Test. The highlights are: -<1> `@RunWith` instructs the spring-test module that it should create an `ApplicationContext`. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-standard[Spring Reference] -<2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#testcontext-ctx-management[Spring Reference] +<1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information, refer to the {spring-framework-reference-url}testing.html#testcontext-junit-jupiter-extension[Spring reference]. +<2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the {spring-framework-reference-url}testing.html#spring-testing-annotation-contextconfiguration[Spring Reference] NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user. It does this by populating the `SecurityContextHolder` prior to running our tests. @@ -225,7 +224,7 @@ For example, the following would run every test with a user with the username "a .Java [source,java,role="primary"] ---- -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration @WithMockUser(username="admin",roles={"USER","ADMIN"}) public class WithMockUserTests { @@ -234,7 +233,7 @@ public class WithMockUserTests { .Kotlin [source,kotlin,role="secondary"] ---- -@RunWith(SpringJUnit4ClassRunner::class) +@ExtendWith(SpringExtension.class) @ContextConfiguration @WithMockUser(username="admin",roles=["USER","ADMIN"]) class WithMockUserTests { @@ -304,7 +303,7 @@ For example, the following will run withMockUser1 and withMockUser2 using < \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/itest/context/spring-security-itest-context.gradle b/itest/context/spring-security-itest-context.gradle index 9e3334454ab..15d323cc9f2 100644 --- a/itest/context/spring-security-itest-context.gradle +++ b/itest/context/spring-security-itest-context.gradle @@ -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" diff --git a/itest/context/src/integration-test/java/org/springframework/security/integration/SEC936ApplicationContextTests.java b/itest/context/src/integration-test/java/org/springframework/security/integration/SEC936ApplicationContextTests.java index cc49be6d4e0..028d94e07e2 100644 --- a/itest/context/src/integration-test/java/org/springframework/security/integration/SEC936ApplicationContextTests.java +++ b/itest/context/src/integration-test/java/org/springframework/security/integration/SEC936ApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -46,7 +46,7 @@ public class SEC936ApplicationContextTests { @Test public void securityInterceptorHandlesCallWithNoTargetObject() { SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("bob", "bobspassword")); + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword")); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.sessionRegistry::getAllPrincipals); } diff --git a/itest/context/src/integration-test/java/org/springframework/security/integration/python/PythonInterpreterBasedSecurityTests.java b/itest/context/src/integration-test/java/org/springframework/security/integration/python/PythonInterpreterBasedSecurityTests.java index 2958b435fc0..df4c34f69d5 100644 --- a/itest/context/src/integration-test/java/org/springframework/security/integration/python/PythonInterpreterBasedSecurityTests.java +++ b/itest/context/src/integration-test/java/org/springframework/security/integration/python/PythonInterpreterBasedSecurityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -35,7 +35,7 @@ public class PythonInterpreterBasedSecurityTests { @Test public void serviceMethod() { SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("bob", "bobspassword")); + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword")); // for (int i=0; i < 1000; i++) { this.service.someMethod(); diff --git a/itest/context/src/integration-test/java/org/springframework/security/performance/FilterChainPerformanceTests.java b/itest/context/src/integration-test/java/org/springframework/security/performance/FilterChainPerformanceTests.java index e53d4f839e6..50260081359 100644 --- a/itest/context/src/integration-test/java/org/springframework/security/performance/FilterChainPerformanceTests.java +++ b/itest/context/src/integration-test/java/org/springframework/security/performance/FilterChainPerformanceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -58,7 +58,7 @@ public class FilterChainPerformanceTests { private static StopWatch sw = new StopWatch("Filter Chain Performance Tests"); - private final UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken("bob", + private final UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.authenticated("bob", "bobspassword", createRoles(N_AUTHORITIES)); private HttpSession session; @@ -129,8 +129,8 @@ public void provideDataOnScalingWithNumberOfAuthoritiesUserHas() throws Exceptio StopWatch sw = new StopWatch("Scaling with nAuthorities"); for (int user = 0; user < N_AUTHORITIES / 10; user++) { int nAuthorities = (user != 0) ? user * 10 : 1; - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken("bob", "bobspassword", createRoles(nAuthorities))); + SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken + .authenticated("bob", "bobspassword", createRoles(nAuthorities))); this.session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); SecurityContextHolder.clearContext(); diff --git a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif index 222e03793c4..ca639f10967 100644 --- a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif +++ b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif @@ -38,6 +38,16 @@ sn: Wombat uid: scott userPassword: wombat +dn: uid=bcrypt,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: BCrypt user +sn: BCrypt +uid: bcrypt +userPassword: $2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u + dn: cn=user,ou=groups,dc=springframework,dc=org objectclass: top objectclass: groupOfNames diff --git a/itest/misc/src/integration-test/java/org/springframework/security/context/SecurityContextHolderMTTests.java b/itest/misc/src/integration-test/java/org/springframework/security/context/SecurityContextHolderMTTests.java index 4a09b0afc33..380941cad6c 100644 --- a/itest/misc/src/integration-test/java/org/springframework/security/context/SecurityContextHolderMTTests.java +++ b/itest/misc/src/integration-test/java/org/springframework/security/context/SecurityContextHolderMTTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -117,7 +117,7 @@ private void loadStartAndWaitForThreads(boolean topLevelThread, String prefix, i } else if (expectAllThreadsToUseIdenticalAuthentication) { // A global SecurityContextHolder.getContext() - .setAuthentication(new UsernamePasswordAuthenticationToken("GLOBAL_USERNAME", + .setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated("GLOBAL_USERNAME", "pass")); for (int i = 0; i < threads.length; i++) { @@ -182,7 +182,7 @@ private Thread makeThread(final String threadIdentifier, final boolean topLevelT public void run() { if (injectAuthIntoCurrentThread) { // Set authentication in this thread - SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken( + SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.authenticated( expectedUsername, "pass")); //System.out.println(threadIdentifier + " - set to " + SecurityContextHolder.getContext().getAuthentication()); diff --git a/itest/web/spring-security-itest-web.gradle b/itest/web/spring-security-itest-web.gradle index 26feb48b14e..4a82c48b079 100644 --- a/itest/web/spring-security-itest-web.gradle +++ b/itest/web/spring-security-itest-web.gradle @@ -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') diff --git a/ldap/spring-security-ldap.gradle b/ldap/spring-security-ldap.gradle index e16802ea452..c4f6c082ed2 100644 --- a/ldap/spring-security-ldap.gradle +++ b/ldap/spring-security-ldap.gradle @@ -8,6 +8,7 @@ dependencies { api 'org.springframework:spring-core' api 'org.springframework:spring-tx' + optional 'com.fasterxml.jackson.core:jackson-databind' optional 'ldapsdk:ldapsdk' optional "com.unboundid:unboundid-ldapsdk" optional "org.apache.directory.server:apacheds-core" @@ -25,7 +26,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" @@ -34,6 +34,7 @@ dependencies { testImplementation "org.mockito:mockito-core" testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" + testImplementation 'org.skyscreamer:jsonassert' } integrationTest { diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/BindAuthenticatorTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/BindAuthenticatorTests.java index bfffaa17de7..789df1813de 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/BindAuthenticatorTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/BindAuthenticatorTests.java @@ -56,14 +56,14 @@ public class BindAuthenticatorTests { public void setUp() { this.authenticator = new BindAuthenticator(this.contextSource); this.authenticator.setMessageSource(new SpringSecurityMessageSource()); - this.bob = new UsernamePasswordAuthenticationToken("bob", "bobspassword"); + this.bob = UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword"); } @Test public void emptyPasswordIsRejected() { - assertThatExceptionOfType(BadCredentialsException.class) - .isThrownBy(() -> this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("jen", ""))); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy( + () -> this.authenticator.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("jen", ""))); } @Test @@ -72,14 +72,15 @@ public void testAuthenticationWithCorrectPasswordSucceeds() { DirContextOperations user = this.authenticator.authenticate(this.bob); assertThat(user.getStringAttribute("uid")).isEqualTo("bob"); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("mouse, jerry", "jerryspassword")); + this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("mouse, jerry", "jerryspassword")); } @Test public void testAuthenticationWithInvalidUserNameFails() { this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.authenticator - .authenticate(new UsernamePasswordAuthenticationToken("nonexistentsuser", "password"))); + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("nonexistentsuser", "password"))); } @Test @@ -93,14 +94,18 @@ public void testAuthenticationWithUserSearch() throws Exception { assertThat(result.getStringAttribute("cn")).isEqualTo("Bob Hamilton"); // SEC-1444 this.authenticator.setUserSearch(new FilterBasedLdapUserSearch("ou=people", "(cn={0})", this.contextSource)); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("mouse, jerry", "jerryspassword")); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("slash/guy", "slashguyspassword")); + this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("mouse, jerry", "jerryspassword")); + this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("slash/guy", "slashguyspassword")); // SEC-1661 this.authenticator.setUserSearch( new FilterBasedLdapUserSearch("ou=\\\"quoted people\\\"", "(cn={0})", this.contextSource)); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("quote\"guy", "quoteguyspassword")); + this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("quote\"guy", "quoteguyspassword")); this.authenticator.setUserSearch(new FilterBasedLdapUserSearch("", "(cn={0})", this.contextSource)); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("quote\"guy", "quoteguyspassword")); + this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("quote\"guy", "quoteguyspassword")); } /* @@ -127,8 +132,8 @@ public void testAuthenticationWithUserSearch() throws Exception { @Test public void testAuthenticationWithWrongPasswordFails() { this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); - assertThatExceptionOfType(BadCredentialsException.class).isThrownBy( - () -> this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("bob", "wrongpassword"))); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("bob", "wrongpassword"))); } @Test diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorTests.java index 645da5c9623..0994a2b4b41 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorTests.java @@ -63,8 +63,8 @@ public void setUp() { this.authenticator = new PasswordComparisonAuthenticator(this.contextSource); this.authenticator.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); - this.bob = new UsernamePasswordAuthenticationToken("bob", "bobspassword"); - this.ben = new UsernamePasswordAuthenticationToken("ben", "benspassword"); + this.bob = UsernamePasswordAuthenticationToken.unauthenticated("bob", "bobspassword"); + this.ben = UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword"); } @Test @@ -81,16 +81,16 @@ public void testFailedSearchGivesUserNotFoundException() throws Exception { .isEmpty(); this.authenticator.setUserSearch(new MockUserSearch(null)); this.authenticator.afterPropertiesSet(); - assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy( - () -> this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("Joe", "pass"))); + assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(() -> this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("Joe", "pass"))); } @Test public void testLdapPasswordCompareFailsWithWrongPassword() { // Don't retrieve the password this.authenticator.setUserAttributes(new String[] { "uid", "cn", "sn" }); - assertThatExceptionOfType(BadCredentialsException.class).isThrownBy( - () -> this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("bob", "wrongpass"))); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("bob", "wrongpass"))); } @Test @@ -131,14 +131,14 @@ public void testPasswordEncoderCantBeNull() { @Test public void testUseOfDifferentPasswordAttributeSucceeds() { this.authenticator.setPasswordAttributeName("uid"); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("bob", "bob")); + this.authenticator.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("bob", "bob")); } @Test public void testLdapCompareWithDifferentPasswordAttributeSucceeds() { this.authenticator.setUserAttributes(new String[] { "uid" }); this.authenticator.setPasswordAttributeName("cn"); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("ben", "Ben Alex")); + this.authenticator.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("ben", "Ben Alex")); } @Test @@ -152,7 +152,8 @@ public void testWithUserSearch() { ctx.setAttributeValue("userPassword", "bobspassword"); this.authenticator.setUserSearch(new MockUserSearch(ctx)); - this.authenticator.authenticate(new UsernamePasswordAuthenticationToken("shouldntbeused", "bobspassword")); + this.authenticator + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("shouldntbeused", "bobspassword")); } } diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManagerTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManagerTests.java index ee2d1be55eb..db29d648807 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManagerTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManagerTests.java @@ -192,8 +192,8 @@ public void testPasswordChangeWithCorrectOldPasswordSucceeds() { this.mgr.createUser(p.createUserDetails()); - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken("johnyossarian", "yossarianspassword", TEST_AUTHORITIES)); + SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken + .authenticated("johnyossarian", "yossarianspassword", TEST_AUTHORITIES)); this.mgr.changePassword("yossarianspassword", "yossariansnewpassword"); @@ -211,8 +211,8 @@ public void testPasswordChangeWithWrongOldPasswordFails() { p.setPassword("yossarianspassword"); p.setAuthorities(TEST_AUTHORITIES); this.mgr.createUser(p.createUserDetails()); - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken("johnyossarian", "yossarianspassword", TEST_AUTHORITIES)); + SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken + .authenticated("johnyossarian", "yossarianspassword", TEST_AUTHORITIES)); assertThatExceptionOfType(BadCredentialsException.class) .isThrownBy(() -> this.mgr.changePassword("wrongpassword", "yossariansnewpassword")); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java index 0d8e8403b73..0471113f6cc 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.log.LogMessage; import org.springframework.ldap.core.support.DirContextAuthenticationStrategy; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; @@ -72,7 +73,7 @@ public DefaultSpringSecurityContextSource(String providerUrl) { String url = tokenizer.nextToken(); String urlRootDn = LdapUtils.parseRootDnFromUrl(url); urls.add(url.substring(0, url.lastIndexOf(urlRootDn))); - this.logger.info(" URL '" + url + "', root DN is '" + urlRootDn + "'"); + this.logger.info(LogMessage.format("Configure with URL %s and root DN %s", url, urlRootDn)); Assert.isTrue(rootDn == null || rootDn.equals(urlRootDn), "Root DNs must be the same when using multiple URLs"); rootDn = (rootDn != null) ? rootDn : urlRootDn; @@ -89,7 +90,7 @@ public void setupEnvironment(Hashtable env, String dn, String password) { // Remove the pooling flag unless authenticating as the 'manager' user. if (!DefaultSpringSecurityContextSource.this.userDn.equals(dn) && env.containsKey(SUN_LDAP_POOLING_FLAG)) { - DefaultSpringSecurityContextSource.this.logger.debug("Removing pooling flag for user " + dn); + DefaultSpringSecurityContextSource.this.logger.trace("Removing pooling flag for user " + dn); env.remove(SUN_LDAP_POOLING_FLAG); } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java b/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java index a222bf48a81..5909eb8f7aa 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java +++ b/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java @@ -53,7 +53,7 @@ public static void closeContext(Context ctx) { } } catch (NamingException ex) { - logger.error("Failed to close context.", ex); + logger.debug("Failed to close context.", ex); } } @@ -64,7 +64,7 @@ public static void closeEnumeration(NamingEnumeration ne) { } } catch (NamingException ex) { - logger.error("Failed to close enumeration.", ex); + logger.debug("Failed to close enumeration.", ex); } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java b/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java index 08c281b1e9b..42d4067b260 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java +++ b/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java @@ -166,7 +166,7 @@ public Set>> searchForMultipleAttributeValues(String ba encodedParams[i] = LdapEncoder.filterEncode(params[i].toString()); } String formattedFilter = MessageFormat.format(filter, encodedParams); - logger.debug(LogMessage.format("Using filter: %s", formattedFilter)); + logger.trace(LogMessage.format("Using filter: %s", formattedFilter)); HashSet>> result = new HashSet<>(); ContextMapper roleMapper = (ctx) -> { DirContextAdapter adapter = (DirContextAdapter) ctx; @@ -223,7 +223,7 @@ private void extractStringAttributeValues(DirContextAdapter adapter, Map stringValues = new ArrayList<>(); @@ -233,9 +233,9 @@ private void extractStringAttributeValues(DirContextAdapter adapter, Map resultsEnum = ctx.search(searchBaseDn, filter, params, buildControls(searchControls)); - logger.debug(LogMessage.format("Searching for entry under DN '%s', base = '%s', filter = '%s'", ctxBaseDn, + logger.trace(LogMessage.format("Searching for entry under DN '%s', base = '%s', filter = '%s'", ctxBaseDn, searchBaseDn, filter)); Set results = new HashSet<>(); try { @@ -284,7 +284,7 @@ public static DirContextOperations searchForSingleEntryInternal(DirContext ctx, } catch (PartialResultException ex) { LdapUtils.closeEnumeration(resultsEnum); - logger.info("Ignoring PartialResultException"); + logger.trace("Ignoring PartialResultException"); } if (results.size() != 1) { throw new IncorrectResultSizeDataAccessException(1, results.size()); diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java index 9a0e1a56ebb..5263f9cecb8 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -24,7 +24,6 @@ import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; -import org.springframework.core.log.LogMessage; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; @@ -68,7 +67,6 @@ public Authentication authenticate(Authentication authentication) throws Authent UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication; String username = userToken.getName(); String password = (String) authentication.getCredentials(); - this.logger.debug(LogMessage.format("Processing authentication request for user: %s", username)); if (!StringUtils.hasLength(username)) { throw new BadCredentialsException( this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username")); @@ -101,9 +99,10 @@ protected Authentication createSuccessfulAuthentication(UsernamePasswordAuthenti UserDetails user) { Object password = this.useAuthenticationRequestCredentials ? authentication.getCredentials() : user.getPassword(); - UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, password, + UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password, this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); + this.logger.debug("Authenticated user"); return result; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java index 1c4fa66eff3..8dc0a39eee4 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java @@ -65,7 +65,7 @@ public DirContextOperations authenticate(Authentication authentication) { String username = authentication.getName(); String password = (String) authentication.getCredentials(); if (!StringUtils.hasLength(password)) { - logger.debug(LogMessage.format("Rejecting empty password for user %s", username)); + logger.debug(LogMessage.format("Failed to authenticate since no credentials provided")); throw new BadCredentialsException( this.messages.getMessage("BindAuthenticator.emptyPassword", "Empty Password")); } @@ -76,11 +76,18 @@ public DirContextOperations authenticate(Authentication authentication) { break; } } + if (user == null) { + logger.debug(LogMessage.of(() -> "Failed to bind with any user DNs " + getUserDns(username))); + } // Otherwise use the configured search object to find the user and authenticate // with the returned DN. if (user == null && getUserSearch() != null) { + logger.trace("Searching for user using " + getUserSearch()); DirContextOperations userFromSearch = getUserSearch().searchForUser(username); user = bindWithDn(userFromSearch.getDn().toString(), username, password, userFromSearch.getAttributes()); + if (user == null) { + logger.debug("Failed to find user using " + getUserSearch()); + } } if (user == null) { throw new BadCredentialsException( @@ -98,13 +105,12 @@ private DirContextOperations bindWithDn(String userDnStr, String username, Strin DistinguishedName userDn = new DistinguishedName(userDnStr); DistinguishedName fullDn = new DistinguishedName(userDn); fullDn.prepend(ctxSource.getBaseLdapPath()); - logger.debug(LogMessage.format("Attempting to bind as %s", fullDn)); + logger.trace(LogMessage.format("Attempting to bind as %s", fullDn)); DirContext ctx = null; try { ctx = getContextSource().getContext(fullDn.toString(), password); // Check for password policy control PasswordPolicyControl ppolicy = PasswordPolicyControlExtractor.extractControl(ctx); - logger.debug("Retrieving attributes..."); if (attrs == null || attrs.size() == 0) { attrs = ctx.getAttributes(userDn, getUserAttributes()); } @@ -112,6 +118,7 @@ private DirContextOperations bindWithDn(String userDnStr, String username, Strin if (ppolicy != null) { result.setAttributeValue(ppolicy.getID(), ppolicy); } + logger.debug(LogMessage.format("Bound %s", fullDn)); return result; } catch (NamingException ex) { @@ -141,7 +148,7 @@ private DirContextOperations bindWithDn(String userDnStr, String username, Strin * logger. */ protected void handleBindException(String userDn, String username, Throwable cause) { - logger.debug(LogMessage.format("Failed to bind as %s: %s", userDn, cause)); + logger.trace(LogMessage.format("Failed to bind as %s", userDn), cause); } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java index a64b8b03682..7d79d358ef9 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java @@ -76,25 +76,37 @@ public DirContextOperations authenticate(final Authentication authentication) { user = ldapTemplate.retrieveEntry(userDn, getUserAttributes()); } catch (NameNotFoundException ignore) { + logger.trace(LogMessage.format("Failed to retrieve user with %s", userDn), ignore); } if (user != null) { break; } } + if (user == null) { + logger.debug(LogMessage.of(() -> "Failed to retrieve user with any user DNs " + getUserDns(username))); + } if (user == null && getUserSearch() != null) { + logger.trace("Searching for user using " + getUserSearch()); user = getUserSearch().searchForUser(username); + if (user == null) { + logger.debug("Failed to find user using " + getUserSearch()); + } } if (user == null) { throw new UsernameNotFoundException("User not found: " + username); } - if (logger.isDebugEnabled()) { - logger.debug(LogMessage.format("Performing LDAP compare of password attribute '%s' for user '%s'", + if (logger.isTraceEnabled()) { + logger.trace(LogMessage.format("Comparing password attribute '%s' for user '%s'", this.passwordAttributeName, user.getDn())); } if (this.usePasswordAttrCompare && isPasswordAttrCompare(user, password)) { + logger.debug(LogMessage.format("Locally matched password attribute '%s' for user '%s'", + this.passwordAttributeName, user.getDn())); return user; } if (isLdapPasswordCompare(user, ldapTemplate, password)) { + logger.debug(LogMessage.format("LDAP-matched password attribute '%s' for user '%s'", + this.passwordAttributeName, user.getDn())); return user; } throw new BadCredentialsException( diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java index 14584623fc3..0db213cfaa9 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java @@ -48,7 +48,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource public String getPrincipal() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { - log.warn("No Authentication object set in SecurityContext - returning empty String as Principal"); + log.debug("Returning empty String as Principal since authentication is null"); return ""; } Object principal = authentication.getPrincipal(); @@ -57,7 +57,7 @@ public String getPrincipal() { return details.getDn(); } if (authentication instanceof AnonymousAuthenticationToken) { - log.debug("Anonymous Authentication, returning empty String as Principal"); + log.debug("Returning empty String as Principal since authentication is anonymous"); return ""; } throw new IllegalArgumentException( @@ -71,7 +71,7 @@ public String getPrincipal() { public String getCredentials() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { - log.warn("No Authentication object set in SecurityContext - returning empty String as Credentials"); + log.debug("Returning empty String as Credentials since authentication is null"); return ""; } return (String) authentication.getCredentials(); diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java new file mode 100644 index 00000000000..fca449114e9 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; + +/** + * This Jackson mixin is used to serialize/deserialize {@link InetOrgPerson}. + * + * @since 5.7 + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class InetOrgPersonMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java new file mode 100644 index 00000000000..85fe16f5fdb --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.LdapAuthority; + +/** + * This Jackson mixin is used to serialize/deserialize {@link LdapAuthority}. + * + * @since 5.7 + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class LdapAuthorityMixin { + + @JsonCreator + LdapAuthorityMixin(@JsonProperty("role") String role, @JsonProperty("dn") String dn, + @JsonProperty("attributes") Map> attributes) { + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java new file mode 100644 index 00000000000..62cb17a11a3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.LdapAuthority; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.Person; + +/** + * Jackson module for {@code spring-security-ldap}. This module registers + * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin}, + * {@link InetOrgPersonMixin}. + * + * If not already enabled, default typing will be automatically enabled as type info is + * required to properly serialize/deserialize objects. In order to use this module just + * add it to your {@code ObjectMapper} configuration. + * + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new LdapJackson2Module());
      + * 
      + * + * Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all + * security modules. + * + * @since 5.7 + * @see SecurityJackson2Modules + */ +public class LdapJackson2Module extends SimpleModule { + + public LdapJackson2Module() { + super(LdapJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void setupModule(SetupContext context) { + SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); + context.setMixInAnnotations(LdapAuthority.class, LdapAuthorityMixin.class); + context.setMixInAnnotations(LdapUserDetailsImpl.class, LdapUserDetailsImplMixin.class); + context.setMixInAnnotations(Person.class, PersonMixin.class); + context.setMixInAnnotations(InetOrgPerson.class, InetOrgPersonMixin.class); + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java new file mode 100644 index 00000000000..a441102e6b3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * This Jackson mixin is used to serialize/deserialize {@link LdapUserDetailsImpl}. + * + * @since 5.7 + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class LdapUserDetailsImplMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java new file mode 100644 index 00000000000..a3a0ddebc57 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.Person; + +/** + * This Jackson mixin is used to serialize/deserialize {@link Person}. + * + * @since 5.7 + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class PersonMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java index 6fb79ffd4fd..9b5d2998818 100755 --- a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java @@ -50,8 +50,7 @@ public DirContext getContext(String principal, String credentials) throws Passwo if (principal.equals(this.userDn)) { return super.getContext(principal, credentials); } - this.logger - .debug(LogMessage.format("Binding as '%s', prior to reconnect as user '%s'", this.userDn, principal)); + this.logger.trace(LogMessage.format("Binding as %s, prior to reconnect as user %s", this.userDn, principal)); // First bind as manager user before rebinding as the specific principal. LdapContext ctx = (LdapContext) super.getContext(this.userDn, this.password); Control[] rctls = { new PasswordPolicyControl(false) }; @@ -63,8 +62,7 @@ public DirContext getContext(String principal, String credentials) throws Passwo catch (javax.naming.NamingException ex) { PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx); if (this.logger.isDebugEnabled()) { - this.logger.debug("Failed to obtain context", ex); - this.logger.debug("Password policy response: " + ctrl); + this.logger.debug(LogMessage.format("Failed to bind with %s", ctrl), ex); } LdapUtils.closeContext(ctx); if (ctrl != null && ctrl.isLocked()) { @@ -72,8 +70,7 @@ public DirContext getContext(String principal, String credentials) throws Passwo } throw LdapUtils.convertLdapException(ex); } - this.logger.debug( - LogMessage.of(() -> "PPolicy control returned: " + PasswordPolicyControlExtractor.extractControl(ctx))); + this.logger.debug(LogMessage.of(() -> "Bound with " + PasswordPolicyControlExtractor.extractControl(ctx))); return ctx; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java index 79f007e4083..adffca9546c 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java @@ -43,7 +43,7 @@ public static PasswordPolicyResponseControl extractControl(DirContext dirCtx) { ctrls = ctx.getResponseControls(); } catch (javax.naming.NamingException ex) { - logger.error("Failed to obtain response controls", ex); + logger.trace("Failed to obtain response controls", ex); } for (int i = 0; ctrls != null && i < ctrls.length; i++) { if (ctrls[i] instanceof PasswordPolicyResponseControl) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java index bb1c8b08988..730b23291aa 100755 --- a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java @@ -31,6 +31,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.log.LogMessage; import org.springframework.dao.DataRetrievalFailureException; /** @@ -158,19 +159,21 @@ public boolean isLocked() { */ @Override public String toString() { - StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl"); + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append(" ["); if (hasError()) { - sb.append(", error: ").append(this.errorStatus.getDefaultMessage()); + sb.append("error=").append(this.errorStatus.getDefaultMessage()).append("; "); } if (this.graceLoginsRemaining != Integer.MAX_VALUE) { - sb.append(", warning: ").append(this.graceLoginsRemaining).append(" grace logins remain"); + sb.append("warning=").append(this.graceLoginsRemaining).append(" grace logins remain; "); } if (this.timeBeforeExpiration != Integer.MAX_VALUE) { - sb.append(", warning: time before expiration is ").append(this.timeBeforeExpiration); + sb.append("warning=time before expiration is ").append(this.timeBeforeExpiration).append("; "); } if (!hasError() && !hasWarning()) { - sb.append(" (no error, no warning)"); + sb.append("(no error, no warning)"); } + sb.append("]"); return sb.toString(); } @@ -192,7 +195,8 @@ public void decode() throws IOException { new ByteArrayInputStream(PasswordPolicyResponseControl.this.encodedValue), bread); int size = seq.size(); if (logger.isDebugEnabled()) { - logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size + " elements"); + logger.debug(LogMessage.format("Received PasswordPolicyResponse whose ASN.1 sequence has %d elements", + size)); } for (int i = 0; i < seq.size(); i++) { BERTag elt = (BERTag) seq.elementAt(i); diff --git a/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java b/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java index 86c6b4e8e75..c00a0b8c498 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java +++ b/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java @@ -79,8 +79,8 @@ public FilterBasedLdapUserSearch(String searchBase, String searchFilter, BaseLda this.searchBase = searchBase; setSearchSubtree(true); if (searchBase.length() == 0) { - logger.info( - "SearchBase not set. Searches will be performed from the root: " + contextSource.getBaseLdapPath()); + logger.info(LogMessage.format("Searches will be performed from the root %s since SearchBase not set", + contextSource.getBaseLdapPath())); } } @@ -93,11 +93,14 @@ public FilterBasedLdapUserSearch(String searchBase, String searchFilter, BaseLda */ @Override public DirContextOperations searchForUser(String username) { - logger.debug(LogMessage.of(() -> "Searching for user '" + username + "', with user search " + this)); + logger.trace(LogMessage.of(() -> "Searching for user '" + username + "', with " + this)); SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(this.contextSource); template.setSearchControls(this.searchControls); try { - return template.searchForSingleEntry(this.searchBase, this.searchFilter, new String[] { username }); + DirContextOperations operations = template.searchForSingleEntry(this.searchBase, this.searchFilter, + new String[] { username }); + logger.debug(LogMessage.of(() -> "Found user '" + username + "', with " + this)); + return operations; } catch (IncorrectResultSizeDataAccessException ex) { if (ex.getActualSize() == 0) { @@ -151,12 +154,14 @@ public void setReturningAttributes(String[] attrs) { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("[ searchFilter: '").append(this.searchFilter).append("', "); - sb.append("searchBase: '").append(this.searchBase).append("'"); - sb.append(", scope: ").append( - (this.searchControls.getSearchScope() != SearchControls.SUBTREE_SCOPE) ? "single-level, " : "subtree"); - sb.append(", searchTimeLimit: ").append(this.searchControls.getTimeLimit()); - sb.append(", derefLinkFlag: ").append(this.searchControls.getDerefLinkFlag()).append(" ]"); + sb.append(getClass().getSimpleName()).append(" ["); + sb.append("searchFilter=").append(this.searchFilter).append("; "); + sb.append("searchBase=").append(this.searchBase).append("; "); + sb.append("scope=").append( + (this.searchControls.getSearchScope() != SearchControls.SUBTREE_SCOPE) ? "single-level" : "subtree") + .append("; "); + sb.append("searchTimeLimit=").append(this.searchControls.getTimeLimit()).append("; "); + sb.append("derefLinkFlag=").append(this.searchControls.getDerefLinkFlag()).append(" ]"); return sb.toString(); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java index eb1eb79093d..379faed9655 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java @@ -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. @@ -77,7 +77,8 @@ * supported with no GA version to replace it. */ @Deprecated -public class ApacheDSContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { +public class ApacheDSContainer + implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -177,10 +178,12 @@ public void setWorkingDirectory(File workingDir) { this.service.setWorkingDirectory(workingDir); } + @Override public void setPort(int port) { this.port = port; } + @Override public int getPort() { return this.port; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java new file mode 100644 index 00000000000..2ca55f44eda --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.server; + +/** + * Provides lifecycle services for an embedded LDAP server. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public interface EmbeddedLdapServerContainer { + + /** + * Returns the embedded LDAP server port. + * @return the embedded LDAP server port + */ + int getPort(); + + /** + * The embedded LDAP server port to connect to. Supplying 0 as the port indicates that + * a random available port should be selected. + * @param port the port to connect to + */ + void setPort(int port); + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java index 269b8adae1f..f8c1d0d84ac 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -38,7 +38,8 @@ /** * @author Eddú Meléndez */ -public class UnboundIdContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { +public class UnboundIdContainer + implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { private InMemoryDirectoryServer directoryServer; @@ -57,10 +58,12 @@ public UnboundIdContainer(String defaultPartitionSuffix, String ldif) { this.ldif = ldif; } + @Override public int getPort() { return this.port; } + @Override public void setPort(int port) { this.port = port; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java index 8ef21554c43..f16ba86b280 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java @@ -163,10 +163,10 @@ public DefaultLdapAuthoritiesPopulator(ContextSource contextSource, String group getLdapTemplate().setSearchControls(getSearchControls()); this.groupSearchBase = groupSearchBase; if (groupSearchBase == null) { - logger.info("groupSearchBase is null. No group search will be performed."); + logger.info("Will not perform group search since groupSearchBase is null."); } else if (groupSearchBase.length() == 0) { - logger.info("groupSearchBase is empty. Searches will be performed from the context source base"); + logger.info("Will perform group search from the context source base since groupSearchBase is empty."); } this.authorityMapper = (record) -> { String role = record.get(this.groupRoleAttribute).get(0); @@ -199,7 +199,6 @@ protected Set getAdditionalRoles(DirContextOperations user, St @Override public final Collection getGrantedAuthorities(DirContextOperations user, String username) { String userDn = user.getNameInNamespace(); - logger.debug(LogMessage.format("Getting authorities for user %s", userDn)); Set roles = getGroupMembershipRoles(userDn, username); Set extraRoles = getAdditionalRoles(user, username); if (extraRoles != null) { @@ -210,6 +209,7 @@ public final Collection getGrantedAuthorities(DirContextOperat } List result = new ArrayList<>(roles.size()); result.addAll(roles); + logger.debug(LogMessage.format("Retrieved authorities for user %s", userDn)); return result; } @@ -218,12 +218,12 @@ public Set getGroupMembershipRoles(String userDn, String usern return new HashSet<>(); } Set authorities = new HashSet<>(); - logger.debug(LogMessage.of(() -> "Searching for roles for user '" + username + "', DN = " + "'" + userDn - + "', with filter " + this.groupSearchFilter + " in search base '" + getGroupSearchBase() + "'")); + logger.trace(LogMessage.of(() -> "Searching for roles for user " + username + " with DN " + userDn + + " and filter " + this.groupSearchFilter + " in search base " + getGroupSearchBase())); Set>> userRoles = getLdapTemplate().searchForMultipleAttributeValues( getGroupSearchBase(), this.groupSearchFilter, new String[] { userDn, username }, new String[] { this.groupRoleAttribute }); - logger.debug(LogMessage.of(() -> "Roles from search: " + userRoles)); + logger.debug(LogMessage.of(() -> "Found roles from search " + userRoles)); for (Map> role : userRoles) { authorities.add(this.authorityMapper.apply(role)); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java index 29a7323e3db..a0ef3b333ba 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java @@ -146,30 +146,16 @@ public int hashCode() { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(super.toString()).append(": "); - sb.append("Dn: ").append(this.dn).append("; "); - sb.append("Username: ").append(this.username).append("; "); - sb.append("Password: [PROTECTED]; "); - sb.append("Enabled: ").append(this.enabled).append("; "); - sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; "); - sb.append("CredentialsNonExpired: ").append(this.credentialsNonExpired).append("; "); - sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; "); - if (this.getAuthorities() != null && !this.getAuthorities().isEmpty()) { - sb.append("Granted Authorities: "); - boolean first = true; - for (Object authority : this.getAuthorities()) { - if (first) { - first = false; - } - else { - sb.append(", "); - } - sb.append(authority.toString()); - } - } - else { - sb.append("Not granted any authorities"); - } + sb.append(getClass().getSimpleName()).append(" ["); + sb.append("Dn=").append(this.dn).append("; "); + sb.append("Username=").append(this.username).append("; "); + sb.append("Password=[PROTECTED]; "); + sb.append("Enabled=").append(this.enabled).append("; "); + sb.append("AccountNonExpired=").append(this.accountNonExpired).append("; "); + sb.append("CredentialsNonExpired=").append(this.credentialsNonExpired).append("; "); + sb.append("AccountNonLocked=").append(this.accountNonLocked).append("; "); + sb.append("Granted Authorities=").append(getAuthorities()); + sb.append("]"); return sb.toString(); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java index 56e724a0755..b44e30a0d87 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java @@ -54,7 +54,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper { public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection authorities) { String dn = ctx.getNameInNamespace(); - this.logger.debug(LogMessage.format("Mapping user details from context with DN: %s", dn)); + this.logger.debug(LogMessage.format("Mapping user details from context with DN %s", dn)); LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence(); essence.setDn(dn); Object passwordValue = ctx.getObjectAttribute(this.passwordAttributeName); @@ -67,7 +67,7 @@ public UserDetails mapUserFromContext(DirContextOperations ctx, String username, String[] rolesForAttribute = ctx.getStringAttributes(this.roleAttributes[i]); if (rolesForAttribute == null) { this.logger.debug( - LogMessage.format("Couldn't read role attribute '%s' for user $s", this.roleAttributes[i], dn)); + LogMessage.format("Couldn't read role attribute %s for user %s", this.roleAttributes[i], dn)); continue; } for (String role : rolesForAttribute) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java index 33d55d7c5b7..b61068ec8f5 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java @@ -166,13 +166,13 @@ public Set getGroupMembershipRoles(String userDn, String usern private void performNestedSearch(String userDn, String username, Set authorities, int depth) { if (depth == 0) { // back out of recursion - logger.debug(LogMessage.of(() -> "Search aborted, max depth reached," + " for roles for user '" + username - + "', DN = " + "'" + userDn + "', with filter " + getGroupSearchFilter() + " in search base '" + logger.debug(LogMessage.of(() -> "Aborted search since max depth reached," + " for roles for user '" + + username + " with DN = " + userDn + " and filter " + getGroupSearchFilter() + " in search base '" + getGroupSearchBase() + "'")); return; } - logger.debug(LogMessage.of(() -> "Searching for roles for user '" + username + "', DN = " + "'" + userDn - + "', with filter " + getGroupSearchFilter() + " in search base '" + getGroupSearchBase() + "'")); + logger.trace(LogMessage.of(() -> "Searching for roles for user " + username + " with DN " + userDn + + " and filter " + getGroupSearchFilter() + " in search base " + getGroupSearchBase())); if (getAttributeNames() == null) { setAttributeNames(new HashSet<>()); } @@ -182,7 +182,7 @@ private void performNestedSearch(String userDn, String username, Set>> userRoles = getLdapTemplate().searchForMultipleAttributeValues( getGroupSearchBase(), getGroupSearchFilter(), new String[] { userDn, username }, getAttributeNames().toArray(new String[0])); - logger.debug(LogMessage.format("Roles from search: %s", userRoles)); + logger.debug(LogMessage.format("Found roles from search %s", userRoles)); for (Map> record : userRoles) { boolean circular = false; String dn = record.get(SpringSecurityLdapTemplate.DN_KEY).get(0); diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/LdapAuthenticationProviderTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/LdapAuthenticationProviderTests.java index c006829f183..092523f2277 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/LdapAuthenticationProviderTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/LdapAuthenticationProviderTests.java @@ -67,16 +67,17 @@ public void testDefaultMapperIsSet() { public void testEmptyOrNullUserNameThrowsException() { LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator(), new MockAuthoritiesPopulator()); - assertThatExceptionOfType(BadCredentialsException.class) - .isThrownBy(() -> ldapProvider.authenticate(new UsernamePasswordAuthenticationToken(null, "password"))); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy( - () -> ldapProvider.authenticate(new UsernamePasswordAuthenticationToken("", "bobspassword"))); + () -> ldapProvider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated(null, "password"))); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> ldapProvider + .authenticate(UsernamePasswordAuthenticationToken.unauthenticated("", "bobspassword"))); } @Test public void usernameNotFoundExceptionIsHiddenByDefault() { final LdapAuthenticator authenticator = mock(LdapAuthenticator.class); - final UsernamePasswordAuthenticationToken joe = new UsernamePasswordAuthenticationToken("joe", "password"); + final UsernamePasswordAuthenticationToken joe = UsernamePasswordAuthenticationToken.unauthenticated("joe", + "password"); given(authenticator.authenticate(joe)).willThrow(new UsernameNotFoundException("nobody")); LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> provider.authenticate(joe)); @@ -85,7 +86,8 @@ public void usernameNotFoundExceptionIsHiddenByDefault() { @Test public void usernameNotFoundExceptionIsNotHiddenIfConfigured() { final LdapAuthenticator authenticator = mock(LdapAuthenticator.class); - final UsernamePasswordAuthenticationToken joe = new UsernamePasswordAuthenticationToken("joe", "password"); + final UsernamePasswordAuthenticationToken joe = UsernamePasswordAuthenticationToken.unauthenticated("joe", + "password"); given(authenticator.authenticate(joe)).willThrow(new UsernameNotFoundException("nobody")); LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); provider.setHideUserNotFoundExceptions(false); @@ -100,7 +102,7 @@ public void normalUsage() { userMapper.setRoleAttributes(new String[] { "ou" }); ldapProvider.setUserDetailsContextMapper(userMapper); assertThat(ldapProvider.getAuthoritiesPopulator()).isNotNull(); - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", + UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword"); Object authDetails = new Object(); authRequest.setDetails(authDetails); @@ -121,7 +123,7 @@ public void passwordIsSetFromUserDataIfUseAuthenticationRequestCredentialsIsFals LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator(), new MockAuthoritiesPopulator()); ldapProvider.setUseAuthenticationRequestCredentials(false); - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", + UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword"); Authentication authResult = ldapProvider.authenticate(authRequest); assertThat(authResult.getCredentials()).isEqualTo("{SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ="); @@ -133,7 +135,7 @@ public void useWithNullAuthoritiesPopulatorReturnsCorrectRole() { LdapUserDetailsMapper userMapper = new LdapUserDetailsMapper(); userMapper.setRoleAttributes(new String[] { "ou" }); ldapProvider.setUserDetailsContextMapper(userMapper); - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", + UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword"); UserDetails user = (UserDetails) ldapProvider.authenticate(authRequest).getPrincipal(); assertThat(user.getAuthorities()).hasSize(1); @@ -142,7 +144,7 @@ public void useWithNullAuthoritiesPopulatorReturnsCorrectRole() { @Test public void authenticateWithNamingException() { - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", + UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated("ben", "benspassword"); LdapAuthenticator mockAuthenticator = mock(LdapAuthenticator.class); CommunicationException expectedCause = new CommunicationException(new javax.naming.CommunicationException()); diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java index 3cc38176eb4..ec5432367c7 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java @@ -53,7 +53,7 @@ public void ldapCompareOperationIsUsedWhenPasswordIsNotRetrieved() throws Except final NamingEnumeration searchResults = new BasicAttributes("", null).getAll(); given(dirCtx.search(eq("cn=Bob,ou=people"), eq("(userPassword={0})"), any(Object[].class), any(SearchControls.class))).willReturn(searchResults); - authenticator.authenticate(new UsernamePasswordAuthenticationToken("Bob", "bobspassword")); + authenticator.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("Bob", "bobspassword")); } } diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java index 4d0a5bc2a6c..e0d28f9392e 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -68,7 +68,7 @@ public class ActiveDirectoryLdapAuthenticationProviderTests { ActiveDirectoryLdapAuthenticationProvider provider; - UsernamePasswordAuthenticationToken joe = new UsernamePasswordAuthenticationToken("joe", "password"); + UsernamePasswordAuthenticationToken joe = UsernamePasswordAuthenticationToken.unauthenticated("joe", "password"); @BeforeEach public void setUp() { @@ -162,7 +162,7 @@ public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws any(SearchControls.class))).willReturn(new MockNamingEnumeration(sr)); this.provider.contextFactory = createContextFactoryReturning(ctx); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.provider.authenticate(this.joe)); - this.provider.authenticate(new UsernamePasswordAuthenticationToken("joe@mydomain.eu", "password")); + this.provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("joe@mydomain.eu", "password")); } @Test @@ -189,8 +189,8 @@ public void noUserSearchCausesUsernameNotFound() throws Exception { // SEC-2500 @Test public void sec2500PreventAnonymousBind() { - assertThatExceptionOfType(BadCredentialsException.class) - .isThrownBy(() -> this.provider.authenticate(new UsernamePasswordAuthenticationToken("rwinch", ""))); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy( + () -> this.provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rwinch", ""))); } @Test diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java new file mode 100644 index 00000000000..d9a05e65313 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link InetOrgPersonMixin}. + */ +public class InetOrgPersonMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String INET_ORG_PERSON_JSON = "{\n" + + "\"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"," + + "\"dn\": \"ignored=ignored\"," + + "\"uid\": \"ghengis\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"carLicense\": \"HORS1\"," + + "\"givenName\": \"Ghengis\"," + + "\"destinationIndicator\": \"West\"," + + "\"displayName\": \"Ghengis McCann\"," + + "\"givenName\": \"Ghengis\"," + + "\"homePhone\": \"+467575436521\"," + + "\"initials\": \"G\"," + + "\"employeeNumber\": \"00001\"," + + "\"homePostalAddress\": \"Steppes\"," + + "\"mail\": \"ghengis@mongolia\"," + + "\"mobile\": \"always\"," + + "\"o\": \"Hordes\"," + + "\"ou\": \"Horde1\"," + + "\"postalAddress\": \"On the Move\"," + + "\"postalCode\": \"Changes Frequently\"," + + "\"roomNumber\": \"Yurt 1\"," + + "\"sn\": \"Khan\"," + + "\"street\": \"Westward Avenue\"," + + "\"telephoneNumber\": \"+442075436521\"," + + "\"departmentNumber\": \"5679\"," + + "\"title\": \"T\"," + + "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]]," + + "\"description\": \"Scary\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(INET_ORG_PERSON_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() + throws JsonProcessingException, JSONException { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(INET_ORG_PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> new ObjectMapper().readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson expectedAuthentication = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + InetOrgPerson authentication = this.mapper.readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getCarLicense()).isEqualTo(expectedAuthentication.getCarLicense()); + assertThat(authentication.getDepartmentNumber()).isEqualTo(expectedAuthentication.getDepartmentNumber()); + assertThat(authentication.getDestinationIndicator()) + .isEqualTo(expectedAuthentication.getDestinationIndicator()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription()); + assertThat(authentication.getDisplayName()).isEqualTo(expectedAuthentication.getDisplayName()); + assertThat(authentication.getUid()).isEqualTo(expectedAuthentication.getUid()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getHomePhone()).isEqualTo(expectedAuthentication.getHomePhone()); + assertThat(authentication.getEmployeeNumber()).isEqualTo(expectedAuthentication.getEmployeeNumber()); + assertThat(authentication.getHomePostalAddress()).isEqualTo(expectedAuthentication.getHomePostalAddress()); + assertThat(authentication.getInitials()).isEqualTo(expectedAuthentication.getInitials()); + assertThat(authentication.getMail()).isEqualTo(expectedAuthentication.getMail()); + assertThat(authentication.getMobile()).isEqualTo(expectedAuthentication.getMobile()); + assertThat(authentication.getO()).isEqualTo(expectedAuthentication.getO()); + assertThat(authentication.getOu()).isEqualTo(expectedAuthentication.getOu()); + assertThat(authentication.getPostalAddress()).isEqualTo(expectedAuthentication.getPostalAddress()); + assertThat(authentication.getPostalCode()).isEqualTo(expectedAuthentication.getPostalCode()); + assertThat(authentication.getRoomNumber()).isEqualTo(expectedAuthentication.getRoomNumber()); + assertThat(authentication.getStreet()).isEqualTo(expectedAuthentication.getStreet()); + assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn()); + assertThat(authentication.getTitle()).isEqualTo(expectedAuthentication.getTitle()); + assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName()); + assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("uid", "ghengis"); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + ctx.setAttributeValue("carLicense", "HORS1"); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("destinationIndicator", "West"); + ctx.setAttributeValue("displayName", "Ghengis McCann"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("homePhone", "+467575436521"); + ctx.setAttributeValue("initials", "G"); + ctx.setAttributeValue("employeeNumber", "00001"); + ctx.setAttributeValue("homePostalAddress", "Steppes"); + ctx.setAttributeValue("mail", "ghengis@mongolia"); + ctx.setAttributeValue("mobile", "always"); + ctx.setAttributeValue("o", "Hordes"); + ctx.setAttributeValue("ou", "Horde1"); + ctx.setAttributeValue("postalAddress", "On the Move"); + ctx.setAttributeValue("postalCode", "Changes Frequently"); + ctx.setAttributeValue("roomNumber", "Yurt 1"); + ctx.setAttributeValue("sn", "Khan"); + ctx.setAttributeValue("street", "Westward Avenue"); + ctx.setAttributeValue("telephoneNumber", "+442075436521"); + ctx.setAttributeValue("departmentNumber", "5679"); + ctx.setAttributeValue("title", "T"); + return ctx; + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java new file mode 100644 index 00000000000..755623ba8f0 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link LdapUserDetailsImplMixin}. + */ +public class LdapUserDetailsImplMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String USER_JSON = "{" + + "\"@class\": \"org.springframework.security.ldap.userdetails.LdapUserDetailsImpl\", " + + "\"dn\": \"ignored=ignored\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(USER_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() + throws JsonProcessingException, JSONException { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(USER_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> new ObjectMapper().readValue(USER_JSON, LdapUserDetailsImpl.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl expectedAuthentication = (LdapUserDetailsImpl) mapper + .mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + + LdapUserDetailsImpl authentication = this.mapper.readValue(USER_JSON, LdapUserDetailsImpl.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + return ctx; + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java new file mode 100644 index 00000000000..018058888ef --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.Person; +import org.springframework.security.ldap.userdetails.PersonContextMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link PersonMixin}. + */ +public class PersonMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String PERSON_JSON = "{" + + "\"@class\": \"org.springframework.security.ldap.userdetails.Person\", " + + "\"dn\": \"ignored=ignored\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"givenName\": \"Ghengis\"," + + "\"sn\": \"Khan\"," + + "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]]," + + "\"description\": \"Scary\"," + + "\"telephoneNumber\": \"+442075436521\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + PersonContextMapper mapper = new PersonContextMapper(); + Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(PERSON_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() + throws JsonProcessingException, JSONException { + PersonContextMapper mapper = new PersonContextMapper(); + Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> new ObjectMapper().readValue(PERSON_JSON, Person.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + PersonContextMapper mapper = new PersonContextMapper(); + Person expectedAuthentication = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + Person authentication = this.mapper.readValue(PERSON_JSON, Person.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn()); + assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName()); + assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("sn", "Khan"); + ctx.setAttributeValue("telephoneNumber", "+442075436521"); + return ctx; + } + +} diff --git a/messaging/spring-security-messaging.gradle b/messaging/spring-security-messaging.gradle index 6556c0e6b00..64435e64dd6 100644 --- a/messaging/spring-security-messaging.gradle +++ b/messaging/spring-security-messaging.gradle @@ -12,10 +12,9 @@ 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' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" @@ -24,7 +23,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" diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java index 4a0e6e8ffaf..5721cb8b447 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.security.messaging.access.expression; +import java.util.function.Supplier; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.messaging.Message; import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; import org.springframework.security.access.expression.SecurityExpressionHandler; @@ -31,15 +35,29 @@ * * @param the type for the body of the Message * @author Rob Winch + * @author Evgeniy Cheban * @since 4.0 */ public class DefaultMessageSecurityExpressionHandler extends AbstractSecurityExpressionHandler> { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, Message message) { + MessageSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, message); + StandardEvaluationContext ctx = new StandardEvaluationContext(root); + ctx.setBeanResolver(getBeanResolver()); + return ctx; + } + @Override protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, Message invocation) { + return createSecurityExpressionRoot(() -> authentication, invocation); + } + + private MessageSecurityExpressionRoot createSecurityExpressionRoot(Supplier authentication, + Message invocation) { MessageSecurityExpressionRoot root = new MessageSecurityExpressionRoot(authentication, invocation); root.setPermissionEvaluator(getPermissionEvaluator()); root.setTrustResolver(this.trustResolver); diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java index ef7a10f438a..baedbf632db 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -27,7 +27,10 @@ * * @author Daniel Bustamante Ospina * @since 5.2 + * @deprecated Since {@link MessageExpressionVoter} is deprecated, there is no more need + * for this class */ +@Deprecated interface EvaluationContextPostProcessor { /** diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java index a819ce4cd31..33e3a52df3d 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -35,7 +35,11 @@ * * @author Rob Winch * @since 4.0 + * @deprecated Use + * {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager} + * instead */ +@Deprecated public final class ExpressionBasedMessageSecurityMetadataSourceFactory { private ExpressionBasedMessageSecurityMetadataSourceFactory() { diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageAuthorizationContextSecurityExpressionHandler.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageAuthorizationContextSecurityExpressionHandler.java new file mode 100644 index 00000000000..934a9d96ffb --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageAuthorizationContextSecurityExpressionHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.messaging.access.expression; + +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.messaging.Message; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.core.Authentication; +import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext; + +/** + * An expression handler for {@link MessageAuthorizationContext}. + * + * @author Josh Cummings + * @since 5.8 + */ +public final class MessageAuthorizationContextSecurityExpressionHandler + implements SecurityExpressionHandler> { + + private final SecurityExpressionHandler> delegate; + + @SuppressWarnings("rawtypes") + public MessageAuthorizationContextSecurityExpressionHandler() { + this(new DefaultMessageSecurityExpressionHandler()); + } + + public MessageAuthorizationContextSecurityExpressionHandler( + SecurityExpressionHandler> expressionHandler) { + this.delegate = expressionHandler; + } + + @Override + public ExpressionParser getExpressionParser() { + return this.delegate.getExpressionParser(); + } + + @Override + public EvaluationContext createEvaluationContext(Authentication authentication, + MessageAuthorizationContext message) { + return createEvaluationContext(() -> authentication, message); + } + + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, + MessageAuthorizationContext message) { + EvaluationContext context = this.delegate.createEvaluationContext(authentication, message.getMessage()); + Map variables = message.getVariables(); + if (variables != null) { + for (Map.Entry entry : variables.entrySet()) { + context.setVariable(entry.getKey(), entry.getValue()); + } + } + return context; + } + +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java index ffa96a22aa9..6e2cbbb7c1c 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -32,7 +32,11 @@ * @author Rob Winch * @author Daniel Bustamante Ospina * @since 4.0 + * @deprecated Use + * {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager} + * instead */ +@Deprecated @SuppressWarnings("serial") class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor> { diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java index b097df8c1e4..2fc1b1c8356 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -37,7 +37,11 @@ * @author Rob Winch * @author Daniel Bustamante Ospina * @since 4.0 + * @deprecated Use + * {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager} + * instead */ +@Deprecated public class MessageExpressionVoter implements AccessDecisionVoter> { private SecurityExpressionHandler> expressionHandler = new DefaultMessageSecurityExpressionHandler<>(); diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java index 710fbeb154b..21f95eeabcd 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.messaging.access.expression; +import java.util.function.Supplier; + import org.springframework.messaging.Message; import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.core.Authentication; @@ -24,6 +26,7 @@ * The {@link SecurityExpressionRoot} used for {@link Message} expressions. * * @author Rob Winch + * @author Evgeniy Cheban * @since 4.0 */ public class MessageSecurityExpressionRoot extends SecurityExpressionRoot { @@ -31,6 +34,17 @@ public class MessageSecurityExpressionRoot extends SecurityExpressionRoot { public final Message message; public MessageSecurityExpressionRoot(Authentication authentication, Message message) { + this(() -> authentication, message); + } + + /** + * Creates an instance for the given {@link Supplier} of the {@link Authentication} + * and {@link Message}. + * @param authentication the {@link Supplier} of the {@link Authentication} to use + * @param message the {@link Message} to use + * @since 5.8 + */ + public MessageSecurityExpressionRoot(Supplier authentication, Message message) { super(authentication); this.message = message; } diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/AuthorizationChannelInterceptor.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/AuthorizationChannelInterceptor.java new file mode 100644 index 00000000000..5ec879f9ccf --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/AuthorizationChannelInterceptor.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.messaging.access.intercept; + +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.log.LogMessage; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +/** + * Authorizes {@link Message} resources using the provided {@link AuthorizationManager} + * + * @author Josh Cummings + * @since 5.8 + */ +public final class AuthorizationChannelInterceptor implements ChannelInterceptor { + + static final Supplier AUTHENTICATION_SUPPLIER = () -> { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new AuthenticationCredentialsNotFoundException( + "An Authentication object was not found in the SecurityContext"); + } + return authentication; + }; + + private final Log logger = LogFactory.getLog(this.getClass()); + + private final AuthorizationManager> preSendAuthorizationManager; + + private AuthorizationEventPublisher eventPublisher = new NoopAuthorizationEventPublisher(); + + /** + * Creates a new instance + * @param preSendAuthorizationManager the {@link AuthorizationManager} to use. Cannot + * be null. + * + */ + public AuthorizationChannelInterceptor(AuthorizationManager> preSendAuthorizationManager) { + Assert.notNull(preSendAuthorizationManager, "preSendAuthorizationManager cannot be null"); + this.preSendAuthorizationManager = preSendAuthorizationManager; + } + + @Override + public Message preSend(Message message, MessageChannel channel) { + this.logger.debug(LogMessage.of(() -> "Authorizing message send")); + AuthorizationDecision decision = this.preSendAuthorizationManager.check(AUTHENTICATION_SUPPLIER, message); + this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, message, decision); + if (decision == null || !decision.isGranted()) { // default deny + this.logger.debug(LogMessage.of(() -> "Failed to authorize message with authorization manager " + + this.preSendAuthorizationManager + " and decision " + decision)); + throw new AccessDeniedException("Access Denied"); + } + this.logger.debug(LogMessage.of(() -> "Authorized message send")); + return message; + } + + /** + * Use this {@link AuthorizationEventPublisher} to publish the + * {@link AuthorizationManager} result. + * @param eventPublisher + */ + public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "eventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + } + + private static class NoopAuthorizationEventPublisher implements AuthorizationEventPublisher { + + @Override + public void publishAuthorizationEvent(Supplier authentication, T object, + AuthorizationDecision decision) { + + } + + } + +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java index 9fe9f7117fc..a0e2f370abe 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -36,7 +36,9 @@ * * @author Rob Winch * @since 4.0 + * @deprecated Use {@link AuthorizationChannelInterceptor} instead */ +@Deprecated public final class ChannelSecurityInterceptor extends AbstractSecurityInterceptor implements ChannelInterceptor { private static final ThreadLocal tokenHolder = new ThreadLocal<>(); diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java index 6e3eb8ba41d..ff896789afe 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -40,7 +40,9 @@ * @since 4.0 * @see ChannelSecurityInterceptor * @see ExpressionBasedMessageSecurityMetadataSourceFactory + * @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead */ +@Deprecated public final class DefaultMessageSecurityMetadataSource implements MessageSecurityMetadataSource { private final Map, Collection> messageMap; diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageAuthorizationContext.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageAuthorizationContext.java new file mode 100644 index 00000000000..cf97421bbf5 --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageAuthorizationContext.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.messaging.access.intercept; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.messaging.Message; + +/** + * An {@link Message} authorization context. + * + * @author Josh Cummings + * @since 5.8 + */ +public final class MessageAuthorizationContext { + + private final Message message; + + private final Map variables; + + /** + * Creates an instance. + * @param message the {@link HttpServletRequest} to use + */ + public MessageAuthorizationContext(Message message) { + this(message, Collections.emptyMap()); + } + + /** + * Creates an instance. + * @param message the {@link HttpServletRequest} to use + * @param variables a map containing key-value pairs representing extracted variable + * names and variable values + */ + public MessageAuthorizationContext(Message message, Map variables) { + this.message = message; + this.variables = variables; + } + + /** + * Returns the {@link HttpServletRequest}. + * @return the {@link HttpServletRequest} to use + */ + public Message getMessage() { + return this.message; + } + + /** + * Returns the extracted variable values where the key is the variable name and the + * value is the variable value. + * @return a map containing key-value pairs representing extracted variable names and + * variable values + */ + public Map getVariables() { + return this.variables; + } + +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java new file mode 100644 index 00000000000..57b06c008e6 --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java @@ -0,0 +1,429 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.messaging.access.intercept; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.log.LogMessage; +import org.springframework.messaging.Message; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.messaging.util.matcher.MessageMatcher; +import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; +import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; + +public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager> { + + private final Log logger = LogFactory.getLog(getClass()); + + private final List>>> mappings; + + private MessageMatcherDelegatingAuthorizationManager( + List>>> mappings) { + Assert.notEmpty(mappings, "mappings cannot be empty"); + this.mappings = mappings; + } + + /** + * Delegates to a specific {@link AuthorizationManager} based on a + * {@link MessageMatcher} evaluation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param message the {@link Message} to check + * @return an {@link AuthorizationDecision}. If there is no {@link MessageMatcher} + * matching the message, or the {@link AuthorizationManager} could not decide, then + * null is returned + */ + @Override + public AuthorizationDecision check(Supplier authentication, Message message) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Authorizing message")); + } + for (Entry>> mapping : this.mappings) { + MessageMatcher matcher = mapping.getMessageMatcher(); + MessageAuthorizationContext authorizationContext = authorizationContext(matcher, message); + if (authorizationContext != null) { + AuthorizationManager> manager = mapping.getEntry(); + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Checking authorization on message using %s", manager)); + } + return manager.check(authentication, authorizationContext); + } + } + this.logger.trace("Abstaining since did not find matching MessageMatcher"); + return null; + } + + private MessageAuthorizationContext authorizationContext(MessageMatcher matcher, Message message) { + if (!matcher.matches((Message) message)) { + return null; + } + if (matcher instanceof SimpDestinationMessageMatcher) { + SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher; + return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message)); + } + return new MessageAuthorizationContext<>(message); + } + + /** + * Creates a builder for {@link MessageMatcherDelegatingAuthorizationManager}. + * @return the new {@link Builder} instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link MessageMatcherDelegatingAuthorizationManager}. + */ + public static final class Builder { + + private final List>>> mappings = new ArrayList<>(); + + private Supplier pathMatcher = () -> new AntPathMatcher(); + + public Builder() { + } + + /** + * Maps any {@link Message} to a security expression. + * @return the Expression to associate + */ + public Builder.Constraint anyMessage() { + return matchers(MessageMatcher.ANY_MESSAGE); + } + + /** + * Maps any {@link Message} that has a null SimpMessageHeaderAccessor destination + * header (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, UNSUBSCRIBE, DISCONNECT, + * DISCONNECT_ACK, OTHER) + * @return the Expression to associate + */ + public Builder.Constraint nullDestMatcher() { + return matchers(SimpDestinationMessageMatcher.NULL_DESTINATION_MATCHER); + } + + /** + * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. + * @param typesToMatch the {@link SimpMessageType} instance to match on + * @return the {@link Builder.Constraint} associated to the matchers. + */ + public Builder.Constraint simpTypeMatchers(SimpMessageType... typesToMatch) { + MessageMatcher[] typeMatchers = new MessageMatcher[typesToMatch.length]; + for (int i = 0; i < typesToMatch.length; i++) { + SimpMessageType typeToMatch = typesToMatch[i]; + typeMatchers[i] = new SimpMessageTypeMatcher(typeToMatch); + } + return matchers(typeMatchers); + } + + /** + * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances without + * regard to the {@link SimpMessageType}. If no destination is found on the + * Message, then the Matcher returns false. + * @param patterns the patterns to create + * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} + * from. + */ + public Builder.Constraint simpDestMatchers(String... patterns) { + return simpDestMatchers(null, patterns); + } + + /** + * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that + * match on {@code SimpMessageType.MESSAGE}. If no destination is found on the + * Message, then the Matcher returns false. + * @param patterns the patterns to create + * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} + * from. + */ + public Builder.Constraint simpMessageDestMatchers(String... patterns) { + return simpDestMatchers(SimpMessageType.MESSAGE, patterns); + } + + /** + * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that + * match on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the + * Message, then the Matcher returns false. + * @param patterns the patterns to create + * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} + * from. + */ + public Builder.Constraint simpSubscribeDestMatchers(String... patterns) { + return simpDestMatchers(SimpMessageType.SUBSCRIBE, patterns); + } + + /** + * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. If no + * destination is found on the Message, then the Matcher returns false. + * @param type the {@link SimpMessageType} to match on. If null, the + * {@link SimpMessageType} is not considered for matching. + * @param patterns the patterns to create + * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} + * from. + * @return the {@link Builder.Constraint} that is associated to the + * {@link MessageMatcher} + */ + private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) { + List> matchers = new ArrayList<>(patterns.length); + for (String pattern : patterns) { + Supplier> supplier = new Builder.PathMatcherMessageMatcherBuilder(pattern, type); + MessageMatcher matcher = new Builder.SupplierMessageMatcher(supplier); + matchers.add(matcher); + } + return new Builder.Constraint(matchers); + } + + /** + * The {@link PathMatcher} to be used with the + * {@link Builder#simpDestMatchers(String...)}. The default is to use the default + * constructor of {@link AntPathMatcher}. + * @param pathMatcher the {@link PathMatcher} to use. Cannot be null. + * @return the {@link Builder} for further customization. + */ + public Builder simpDestPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "pathMatcher cannot be null"); + this.pathMatcher = () -> pathMatcher; + return this; + } + + /** + * The {@link PathMatcher} to be used with the + * {@link Builder#simpDestMatchers(String...)}. Use this method to delay the + * computation or lookup of the {@link PathMatcher}. + * @param pathMatcher the {@link PathMatcher} to use. Cannot be null. + * @return the {@link Builder} for further customization. + */ + public Builder simpDestPathMatcher(Supplier pathMatcher) { + Assert.notNull(pathMatcher, "pathMatcher cannot be null"); + this.pathMatcher = pathMatcher; + return this; + } + + /** + * Maps a {@link List} of {@link MessageMatcher} instances to a security + * expression. + * @param matchers the {@link MessageMatcher} instances to map. + * @return The {@link Builder.Constraint} that is associated to the + * {@link MessageMatcher} instances + */ + public Builder.Constraint matchers(MessageMatcher... matchers) { + List> builders = new ArrayList<>(matchers.length); + for (MessageMatcher matcher : matchers) { + builders.add(matcher); + } + return new Builder.Constraint(builders); + } + + public AuthorizationManager> build() { + return new MessageMatcherDelegatingAuthorizationManager(this.mappings); + } + + /** + * Represents the security constraint to be applied to the {@link MessageMatcher} + * instances. + */ + public final class Constraint { + + private final List> messageMatchers; + + /** + * Creates a new instance + * @param messageMatchers the {@link MessageMatcher} instances to map to this + * constraint + */ + private Constraint(List> messageMatchers) { + Assert.notEmpty(messageMatchers, "messageMatchers cannot be null or empty"); + this.messageMatchers = messageMatchers; + } + + /** + * Shortcut for specifying {@link Message} instances require a particular + * role. If you do not want to have "ROLE_" automatically inserted see + * {@link #hasAuthority(String)}. + * @param role the role to require (i.e. USER, ADMIN, etc). Note, it should + * not start with "ROLE_" as this is automatically inserted. + * @return the {@link Builder} for further customization + */ + public Builder hasRole(String role) { + return access(AuthorityAuthorizationManager.hasRole(role)); + } + + /** + * Shortcut for specifying {@link Message} instances require any of a number + * of roles. If you do not want to have "ROLE_" automatically inserted see + * {@link #hasAnyAuthority(String...)} + * @param roles the roles to require (i.e. USER, ADMIN, etc). Note, it should + * not start with "ROLE_" as this is automatically inserted. + * @return the {@link Builder} for further customization + */ + public Builder hasAnyRole(String... roles) { + return access(AuthorityAuthorizationManager.hasAnyRole(roles)); + } + + /** + * Specify that {@link Message} instances require a particular authority. + * @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, + * etc). + * @return the {@link Builder} for further customization + */ + public Builder hasAuthority(String authority) { + return access(AuthorityAuthorizationManager.hasAuthority(authority)); + } + + /** + * Specify that {@link Message} instances requires any of a number + * authorities. + * @param authorities the requests require at least one of the authorities + * (i.e. "ROLE_USER","ROLE_ADMIN" would mean either "ROLE_USER" or + * "ROLE_ADMIN" is required). + * @return the {@link Builder} for further customization + */ + public Builder hasAnyAuthority(String... authorities) { + return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); + } + + /** + * Specify that Messages are allowed by anyone. + * @return the {@link Builder} for further customization + */ + public Builder permitAll() { + return access((authentication, context) -> new AuthorizationDecision(true)); + } + + /** + * Specify that Messages are not allowed by anyone. + * @return the {@link Builder} for further customization + */ + public Builder denyAll() { + return access((authorization, context) -> new AuthorizationDecision(false)); + } + + /** + * Specify that Messages are allowed by any authenticated user. + * @return the {@link Builder} for further customization + */ + public Builder authenticated() { + return access(AuthenticatedAuthorizationManager.authenticated()); + } + + /** + * Allows specifying that Messages are secured by an arbitrary expression + * @param authorizationManager the {@link AuthorizationManager} to secure the + * destinations + * @return the {@link Builder} for further customization + */ + public Builder access(AuthorizationManager> authorizationManager) { + for (MessageMatcher messageMatcher : this.messageMatchers) { + Builder.this.mappings.add(new Entry<>(messageMatcher, authorizationManager)); + } + return Builder.this; + } + + } + + private static final class SupplierMessageMatcher implements MessageMatcher { + + private final Supplier> supplier; + + private volatile MessageMatcher delegate; + + SupplierMessageMatcher(Supplier> supplier) { + this.supplier = supplier; + } + + @Override + public boolean matches(Message message) { + if (this.delegate == null) { + synchronized (this.supplier) { + if (this.delegate == null) { + this.delegate = this.supplier.get(); + } + } + } + return this.delegate.matches(message); + } + + } + + private final class PathMatcherMessageMatcherBuilder implements Supplier> { + + private final String pattern; + + private final SimpMessageType type; + + private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) { + this.pattern = pattern; + this.type = type; + } + + private PathMatcher resolvePathMatcher() { + return Builder.this.pathMatcher.get(); + } + + @Override + public MessageMatcher get() { + PathMatcher pathMatcher = resolvePathMatcher(); + if (this.type == null) { + return new SimpDestinationMessageMatcher(this.pattern, pathMatcher); + } + if (SimpMessageType.MESSAGE == this.type) { + return SimpDestinationMessageMatcher.createMessageMatcher(this.pattern, pathMatcher); + } + if (SimpMessageType.SUBSCRIBE == this.type) { + return SimpDestinationMessageMatcher.createSubscribeMatcher(this.pattern, pathMatcher); + } + throw new IllegalStateException(this.type + " is not supported since it does not have a destination"); + } + + } + + } + + private static final class Entry { + + private final MessageMatcher messageMatcher; + + private final T entry; + + Entry(MessageMatcher requestMatcher, T entry) { + this.messageMatcher = requestMatcher; + this.entry = entry; + } + + MessageMatcher getMessageMatcher() { + return this.messageMatcher; + } + + T getEntry() { + return this.entry; + } + + } + +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java index acf6565c457..ee3a30f9b4f 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,9 @@ * @since 4.0 * @see ChannelSecurityInterceptor * @see DefaultMessageSecurityMetadataSource + * @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead */ +@Deprecated public interface MessageSecurityMetadataSource extends SecurityMetadataSource { } diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandlerTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandlerTests.java index 52d83897efb..9e62e6a15d0 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandlerTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.security.messaging.access.expression; +import java.util.function.Supplier; + +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,10 +27,12 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.ExpressionUtils; +import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolver; @@ -38,6 +43,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; @ExtendWith(MockitoExtension.class) public class DefaultMessageSecurityExpressionHandlerTests { @@ -104,4 +112,16 @@ public void permissionEvaluator() { assertThat(ExpressionUtils.evaluateAsBoolean(expression, context)).isTrue(); } + @Test + public void createEvaluationContextSupplierAuthentication() { + Supplier mockAuthenticationSupplier = mock(Supplier.class); + given(mockAuthenticationSupplier.get()).willReturn(this.authentication); + EvaluationContext context = this.handler.createEvaluationContext(mockAuthenticationSupplier, this.message); + verifyNoInteractions(mockAuthenticationSupplier); + assertThat(context.getRootObject()).extracting(TypedValue::getValue) + .asInstanceOf(InstanceOfAssertFactories.type(MessageSecurityExpressionRoot.class)) + .extracting(SecurityExpressionRoot::getAuthentication).isEqualTo(this.authentication); + verify(mockAuthenticationSupplier).get(); + } + } diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/intercept/AuthorizationChannelInterceptorTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/intercept/AuthorizationChannelInterceptorTests.java new file mode 100644 index 00000000000..60528995c95 --- /dev/null +++ b/messaging/src/test/java/org/springframework/security/messaging/access/intercept/AuthorizationChannelInterceptorTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.messaging.access.intercept; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthorizationChannelInterceptor} + */ +@ExtendWith(MockitoExtension.class) +public class AuthorizationChannelInterceptorTests { + + @Mock + Message message; + + @Mock + MessageChannel channel; + + @Mock + AuthorizationManager> authorizationManager; + + @Mock + AuthorizationEventPublisher eventPublisher; + + Authentication originalAuth; + + AuthorizationChannelInterceptor interceptor; + + @BeforeEach + public void setup() { + this.interceptor = new AuthorizationChannelInterceptor(this.authorizationManager); + this.originalAuth = new TestingAuthenticationToken("user", "pass", "ROLE_USER"); + SecurityContextHolder.getContext().setAuthentication(this.originalAuth); + } + + @AfterEach + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void constructorWhenAuthorizationManagerNullThenIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> new AuthorizationChannelInterceptor(null)); + } + + @Test + public void preSendWhenAllowThenSameMessage() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true)); + assertThat(this.interceptor.preSend(this.message, this.channel)).isSameAs(this.message); + } + + @Test + public void preSendWhenDenyThenException() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.interceptor.preSend(this.message, this.channel)); + } + + @Test + public void setEventPublisherWhenNullThenException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.interceptor.setAuthorizationEventPublisher(null)); + } + + @Test + public void preSendWhenAuthorizationEventPublisherThenPublishes() { + this.interceptor.setAuthorizationEventPublisher(this.eventPublisher); + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true)); + this.interceptor.preSend(this.message, this.channel); + verify(this.eventPublisher).publishAuthorizationEvent(any(), any(), any()); + } + +} diff --git a/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java b/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java index 1901b87aaa8..774c13530ad 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java @@ -69,6 +69,7 @@ public void setup() { @AfterEach public void cleanup() { + this.interceptor.afterMessageHandled(this.messageBuilder.build(), this.channel, this.handler, null); SecurityContextHolder.clearContext(); } diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 3a6b1c1394a..7b05d9c46ce 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -23,8 +23,8 @@ 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-spi-test:0.8.6.RELEASE' + testImplementation 'io.r2dbc:r2dbc-h2:0.9.1.RELEASE' + testImplementation 'io.r2dbc:r2dbc-spi-test:0.9.1.RELEASE' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" @@ -35,5 +35,5 @@ dependencies { testRuntimeOnly 'org.hsqldb:hsqldb' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java index 87accc63a32..857f38af0b6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,6 +19,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient; @@ -45,6 +46,8 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth private OAuth2AccessTokenResponseClient accessTokenResponseClient = new DefaultJwtBearerTokenResponseClient(); + private Function jwtAssertionResolver = this::resolveJwtAssertion; + private Duration clockSkew = Duration.ofSeconds(60); private Clock clock = Clock.systemUTC(); @@ -75,10 +78,10 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { // need for re-authorization return null; } - if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { + Jwt jwt = this.jwtAssertionResolver.apply(context); + if (jwt == null) { return null; } - Jwt jwt = (Jwt) context.getPrincipal().getPrincipal(); // As per spec, in section 4.1 Using Assertions as Authorization Grants // https://tools.ietf.org/html/rfc7521#section-4.1 // @@ -97,6 +100,13 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { tokenResponse.getAccessToken()); } + private Jwt resolveJwtAssertion(OAuth2AuthorizationContext context) { + if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { + return null; + } + return (Jwt) context.getPrincipal().getPrincipal(); + } + private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration, JwtBearerGrantRequest jwtBearerGrantRequest) { try { @@ -123,6 +133,17 @@ public void setAccessTokenResponseClient( this.accessTokenResponseClient = accessTokenResponseClient; } + /** + * Sets the resolver used for resolving the {@link Jwt} assertion. + * @param jwtAssertionResolver the resolver used for resolving the {@link Jwt} + * assertion + * @since 5.7 + */ + public void setJwtAssertionResolver(Function jwtAssertionResolver) { + Assert.notNull(jwtAssertionResolver, "jwtAssertionResolver cannot be null"); + this.jwtAssertionResolver = jwtAssertionResolver; + } + /** * Sets the maximum acceptable clock skew, which is used when checking the * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java index eb60c3c4bb8..a15da34b3c0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,6 +19,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import reactor.core.publisher.Mono; @@ -45,6 +46,8 @@ public final class JwtBearerReactiveOAuth2AuthorizedClientProvider implements Re private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = new WebClientReactiveJwtBearerTokenResponseClient(); + private Function> jwtAssertionResolver = this::resolveJwtAssertion; + private Duration clockSkew = Duration.ofSeconds(60); private Clock clock = Clock.systemUTC(); @@ -74,10 +77,7 @@ public Mono authorize(OAuth2AuthorizationContext context // need for re-authorization return Mono.empty(); } - if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { - return Mono.empty(); - } - Jwt jwt = (Jwt) context.getPrincipal().getPrincipal(); + // As per spec, in section 4.1 Using Assertions as Authorization Grants // https://tools.ietf.org/html/rfc7521#section-4.1 // @@ -90,13 +90,26 @@ public Mono authorize(OAuth2AuthorizationContext context // issued with a reasonably short lifetime. Clients can refresh an // expired access token by requesting a new one using the same // assertion, if it is still valid, or with a new assertion. - return Mono.just(new JwtBearerGrantRequest(clientRegistration, jwt)) + + // @formatter:off + return this.jwtAssertionResolver.apply(context) + .map((jwt) -> new JwtBearerGrantRequest(clientRegistration, jwt)) .flatMap(this.accessTokenResponseClient::getTokenResponse) .onErrorMap(OAuth2AuthorizationException.class, (ex) -> new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex)) .map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken())); + // @formatter:on + } + + private Mono resolveJwtAssertion(OAuth2AuthorizationContext context) { + // @formatter:off + return Mono.just(context) + .map((ctx) -> ctx.getPrincipal().getPrincipal()) + .filter((principal) -> principal instanceof Jwt) + .cast(Jwt.class); + // @formatter:on } private boolean hasTokenExpired(OAuth2Token token) { @@ -115,6 +128,17 @@ public void setAccessTokenResponseClient( this.accessTokenResponseClient = accessTokenResponseClient; } + /** + * Sets the resolver used for resolving the {@link Jwt} assertion. + * @param jwtAssertionResolver the resolver used for resolving the {@link Jwt} + * assertion + * @since 5.7 + */ + public void setJwtAssertionResolver(Function> jwtAssertionResolver) { + Assert.notNull(jwtAssertionResolver, "jwtAssertionResolver cannot be null"); + this.jwtAssertionResolver = jwtAssertionResolver; + } + /** * Sets the maximum acceptable clock skew, which is used when checking the * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index fa109dd2aa3..10a048f185f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java @@ -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 PasswordGrantBuilder accessTokenResponseClient( /** * 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 ClientCredentialsGrantBuilder accessTokenResponseClient( /** * 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 RefreshTokenGrantBuilder accessTokenResponseClient( /** * 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; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java index 7b0580571db..c9483fa16be 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java @@ -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 ClientCredentialsGrantBuilder accessTokenResponseClient( /** * 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 PasswordGrantBuilder accessTokenResponseClient( /** * 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 RefreshTokenGrantBuilder accessTokenResponseClient( /** * 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; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java index 03c8611af30..6520f24792c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -22,6 +22,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.function.Function; import com.nimbusds.jose.jwk.JWK; @@ -62,6 +63,7 @@ * * @param the type of {@link AbstractOAuth2AuthorizationGrantRequest} * @author Joe Grandja + * @author Steve Riesenberg * @since 5.5 * @see Converter * @see com.nimbusds.jose.jwk.JWK @@ -87,6 +89,9 @@ public final class NimbusJwtClientAuthenticationParametersConverter jwsEncoders = new ConcurrentHashMap<>(); + private Consumer> jwtClientAssertionCustomizer = (context) -> { + }; + /** * Constructs a {@code NimbusJwtClientAuthenticationParametersConverter} using the * provided parameters. @@ -142,6 +147,10 @@ public MultiValueMap convert(T authorizationGrantRequest) { .expiresAt(expiresAt); // @formatter:on + JwtClientAuthenticationContext jwtClientAssertionContext = new JwtClientAuthenticationContext<>( + authorizationGrantRequest, headersBuilder, claimsBuilder); + this.jwtClientAssertionCustomizer.accept(jwtClientAssertionContext); + JwsHeader jwsHeader = headersBuilder.build(); JwtClaimsSet jwtClaimsSet = claimsBuilder.build(); @@ -189,6 +198,21 @@ else if (KeyType.OCT.equals(jwk.getKeyType())) { return jwsAlgorithm; } + /** + * Sets the {@link Consumer} to be provided the + * {@link JwtClientAuthenticationContext}, which contains the + * {@link JwsHeader.Builder} and {@link JwtClaimsSet.Builder} for further + * customization. + * @param jwtClientAssertionCustomizer the {@link Consumer} to be provided the + * {@link JwtClientAuthenticationContext} + * @since 5.7 + */ + public void setJwtClientAssertionCustomizer( + Consumer> jwtClientAssertionCustomizer) { + Assert.notNull(jwtClientAssertionCustomizer, "jwtClientAssertionCustomizer cannot be null"); + this.jwtClientAssertionCustomizer = jwtClientAssertionCustomizer; + } + private static final class JwsEncoderHolder { private final JwtEncoder jwsEncoder; @@ -210,4 +234,59 @@ private JWK getJwk() { } + /** + * A context that holds client authentication-specific state and is used by + * {@link NimbusJwtClientAuthenticationParametersConverter} when attempting to + * customize the JSON Web Token (JWS) client assertion. + * + * @param the type of {@link AbstractOAuth2AuthorizationGrantRequest} + * @since 5.7 + */ + public static final class JwtClientAuthenticationContext { + + private final T authorizationGrantRequest; + + private final JwsHeader.Builder headers; + + private final JwtClaimsSet.Builder claims; + + private JwtClientAuthenticationContext(T authorizationGrantRequest, JwsHeader.Builder headers, + JwtClaimsSet.Builder claims) { + this.authorizationGrantRequest = authorizationGrantRequest; + this.headers = headers; + this.claims = claims; + } + + /** + * Returns the {@link AbstractOAuth2AuthorizationGrantRequest authorization grant + * request}. + * @return the {@link AbstractOAuth2AuthorizationGrantRequest authorization grant + * request} + */ + public T getAuthorizationGrantRequest() { + return this.authorizationGrantRequest; + } + + /** + * Returns the {@link JwsHeader.Builder} to be used to customize headers of the + * JSON Web Token (JWS). + * @return the {@link JwsHeader.Builder} to be used to customize headers of the + * JSON Web Token (JWS) + */ + public JwsHeader.Builder getHeaders() { + return this.headers; + } + + /** + * Returns the {@link JwtClaimsSet.Builder} to be used to customize claims of the + * JSON Web Token (JWS). + * @return the {@link JwtClaimsSet.Builder} to be used to customize claims of the + * JSON Web Token (JWS) + */ + public JwtClaimsSet.Builder getClaims() { + return this.claims; + } + + } + } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java index 3f23c7d5836..5e14fb66a19 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java @@ -23,10 +23,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.ResponseErrorHandler; @@ -41,7 +43,7 @@ */ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler { - private final OAuth2ErrorHttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter(); + private HttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter(); private final ResponseErrorHandler defaultErrorHandler = new DefaultResponseErrorHandler(); @@ -89,4 +91,15 @@ private BearerTokenError getBearerToken(String wwwAuthenticateHeader) { } } + /** + * Sets the {@link HttpMessageConverter} for an OAuth 2.0 Error. + * @param oauth2ErrorConverter A {@link HttpMessageConverter} for an + * {@link OAuth2Error OAuth 2.0 Error}. + * @since 5.7 + */ + public final void setErrorConverter(HttpMessageConverter oauth2ErrorConverter) { + Assert.notNull(oauth2ErrorConverter, "oauth2ErrorConverter cannot be null"); + this.oauth2ErrorConverter = oauth2ErrorConverter; + } + } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java index 4c8158247a9..ba1eaacd2c7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.java @@ -16,8 +16,6 @@ package org.springframework.security.oauth2.client.jackson2; -import java.util.Collections; - import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -97,8 +95,6 @@ public OAuth2ClientJackson2Module() { @Override public void setupModule(SetupContext context) { SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); - context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(), - UnmodifiableMapMixin.class); context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class); context.setMixInAnnotations(ClientRegistration.class, ClientRegistrationMixin.class); context.setMixInAnnotations(OAuth2AccessToken.class, OAuth2AccessTokenMixin.class); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java index 262e08a2aae..6b335ad4630 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java @@ -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 @@ import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -67,7 +68,7 @@ protected String determineTargetUrl(HttpServletRequest request, HttpServletRespo URI endSessionEndpoint = this.endSessionEndpoint(clientRegistration); if (endSessionEndpoint != null) { String idToken = idToken(authentication); - String postLogoutRedirectUri = postLogoutRedirectUri(request); + String postLogoutRedirectUri = postLogoutRedirectUri(request, clientRegistration); targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri); } } @@ -89,7 +90,7 @@ private String idToken(Authentication authentication) { return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); } - private String postLogoutRedirectUri(HttpServletRequest request) { + private String postLogoutRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) { if (this.postLogoutRedirectUri == null) { return null; } @@ -100,8 +101,13 @@ private String postLogoutRedirectUri(HttpServletRequest request) { .replaceQuery(null) .fragment(null) .build(); + + Map uriVariables = new HashMap<>(); + uriVariables.put("baseUrl", uriComponents.toUriString()); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri) - .buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString())) + .buildAndExpand(uriVariables) .toUriString(); // @formatter:on } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java index 7e606eae6bd..915730dbea3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -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. @@ -34,7 +34,6 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.web.util.UrlUtils; @@ -70,14 +69,18 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au private static final char PATH_DELIMITER = '/'; - private final ClientRegistrationRepository clientRegistrationRepository; + private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder()); - private final AntPathRequestMatcher authorizationRequestMatcher; + private static final StringKeyGenerator DEFAULT_SECURE_KEY_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder().withoutPadding(), 96); - private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private static final Consumer DEFAULT_PKCE_APPLIER = OAuth2AuthorizationRequestCustomizers + .withPkce(); - private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator( - Base64.getUrlEncoder().withoutPadding(), 96); + private final ClientRegistrationRepository clientRegistrationRepository; + + private final AntPathRequestMatcher authorizationRequestMatcher; private Consumer authorizationRequestCustomizer = (customizer) -> { }; @@ -100,7 +103,7 @@ public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository cl @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { - String registrationId = this.resolveRegistrationId(request); + String registrationId = resolveRegistrationId(request); if (registrationId == null) { return null; } @@ -123,6 +126,7 @@ public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String reg * @param authorizationRequestCustomizer the {@code Consumer} to be provided the * {@link OAuth2AuthorizationRequest.Builder} * @since 5.3 + * @see OAuth2AuthorizationRequestCustomizers */ public void setAuthorizationRequestCustomizer( Consumer authorizationRequestCustomizer) { @@ -147,9 +151,7 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re if (clientRegistration == null) { throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); } - Map attributes = new HashMap<>(); - attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); - OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration, attributes); + OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration); String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction); @@ -158,8 +160,7 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) .redirectUri(redirectUriStr) .scopes(clientRegistration.getScopes()) - .state(this.stateGenerator.generateKey()) - .attributes(attributes); + .state(DEFAULT_STATE_GENERATOR.generateKey()); // @formatter:on this.authorizationRequestCustomizer.accept(builder); @@ -167,23 +168,24 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re return builder.build(); } - private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration, - Map attributes) { + private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration) { if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { - OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode(); - Map additionalParameters = new HashMap<>(); + // @formatter:off + OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode() + .attributes((attrs) -> + attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + // @formatter:on if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && clientRegistration.getScopes().contains(OidcScopes.OPENID)) { // Section 3.1.2.1 Authentication Request - // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope // REQUIRED. OpenID Connect requests MUST contain the "openid" scope // value. - addNonceParameters(attributes, additionalParameters); + applyNonce(builder); } if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { - addPkceParameters(attributes, additionalParameters); + DEFAULT_PKCE_APPLIER.accept(builder); } - builder.additionalParameters(additionalParameters); return builder; } if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { @@ -252,54 +254,22 @@ private static String expandRedirectUri(HttpServletRequest request, ClientRegist /** * Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests. - * @param attributes where the {@link OidcParameterNames#NONCE} is stored for the - * authentication request - * @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is - * added for the authentication request + * @param builder where the {@link OidcParameterNames#NONCE} and hash is stored for + * the authentication request * * @since 5.2 * @see 3.1.2.1. * Authentication Request */ - private void addNonceParameters(Map attributes, Map additionalParameters) { + private static void applyNonce(OAuth2AuthorizationRequest.Builder builder) { try { - String nonce = this.secureKeyGenerator.generateKey(); + String nonce = DEFAULT_SECURE_KEY_GENERATOR.generateKey(); String nonceHash = createHash(nonce); - attributes.put(OidcParameterNames.NONCE, nonce); - additionalParameters.put(OidcParameterNames.NONCE, nonceHash); - } - catch (NoSuchAlgorithmException ex) { - } - } - - /** - * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization - * and Access Token Requests - * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the - * token request - * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, - * usually, {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in - * the authorization request. - * - * @since 5.2 - * @see 1.1. - * Protocol Flow - * @see 4.1. - * Client Creates a Code Verifier - * @see 4.2. - * Client Creates the Code Challenge - */ - private void addPkceParameters(Map attributes, Map additionalParameters) { - String codeVerifier = this.secureKeyGenerator.generateKey(); - attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); - try { - String codeChallenge = createHash(codeVerifier); - additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); - additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + builder.attributes((attrs) -> attrs.put(OidcParameterNames.NONCE, nonce)); + builder.additionalParameters((params) -> params.put(OidcParameterNames.NONCE, nonceHash)); } catch (NoSuchAlgorithmException ex) { - additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestCustomizers.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestCustomizers.java new file mode 100644 index 00000000000..b1c71be8a38 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestCustomizers.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.web; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; + +/** + * A factory of customizers that customize the {@link OAuth2AuthorizationRequest OAuth 2.0 + * Authorization Request} via the {@link OAuth2AuthorizationRequest.Builder}. + * + * @author Joe Grandja + * @since 5.7 + * @see OAuth2AuthorizationRequest.Builder + * @see DefaultOAuth2AuthorizationRequestResolver#setAuthorizationRequestCustomizer(Consumer) + * @see DefaultServerOAuth2AuthorizationRequestResolver#setAuthorizationRequestCustomizer(Consumer) + */ +public final class OAuth2AuthorizationRequestCustomizers { + + private static final StringKeyGenerator DEFAULT_SECURE_KEY_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder().withoutPadding(), 96); + + private OAuth2AuthorizationRequestCustomizers() { + } + + /** + * Returns a {@code Consumer} to be provided the + * {@link OAuth2AuthorizationRequest.Builder} that adds the + * {@link PkceParameterNames#CODE_CHALLENGE code_challenge} and, usually, + * {@link PkceParameterNames#CODE_CHALLENGE_METHOD code_challenge_method} parameters + * to the OAuth 2.0 Authorization Request. The {@code code_verifier} is stored in + * {@link OAuth2AuthorizationRequest#getAttribute(String)} under the key + * {@link PkceParameterNames#CODE_VERIFIER code_verifier} for subsequent use in the + * OAuth 2.0 Access Token Request. + * @return a {@code Consumer} to be provided the + * {@link OAuth2AuthorizationRequest.Builder} that adds the PKCE parameters + * @see 1.1. Protocol Flow + * @see 4.1. Client Creates a + * Code Verifier + * @see 4.2. Client Creates the + * Code Challenge + */ + public static Consumer withPkce() { + return OAuth2AuthorizationRequestCustomizers::applyPkce; + } + + private static void applyPkce(OAuth2AuthorizationRequest.Builder builder) { + if (isPkceAlreadyApplied(builder)) { + return; + } + + String codeVerifier = DEFAULT_SECURE_KEY_GENERATOR.generateKey(); + + builder.attributes((attrs) -> attrs.put(PkceParameterNames.CODE_VERIFIER, codeVerifier)); + + builder.additionalParameters((params) -> { + try { + String codeChallenge = createHash(codeVerifier); + params.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); + params.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + } + catch (NoSuchAlgorithmException ex) { + params.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); + } + }); + } + + private static boolean isPkceAlreadyApplied(OAuth2AuthorizationRequest.Builder builder) { + AtomicBoolean pkceApplied = new AtomicBoolean(false); + builder.additionalParameters((params) -> { + if (params.containsKey(PkceParameterNames.CODE_CHALLENGE)) { + pkceApplied.set(true); + } + }); + return pkceApplied.get(); + } + + private static String createHash(String value) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 9bebb0869c8..fa53214d49a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -222,6 +222,7 @@ public final void setAuthorizationRequestRepository( * authentication result. * @param authenticationResultConverter the converter for * {@link OAuth2AuthenticationToken}'s + * @since 5.6 */ public final void setAuthenticationResultConverter( Converter authenticationResultConverter) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java index a3a0169189e..b5f557bffe0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java @@ -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. @@ -32,11 +32,11 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; @@ -59,6 +59,7 @@ * * @author Rob Winch * @author Mark Heckler + * @author Joe Grandja * @since 5.1 */ public class DefaultServerOAuth2AuthorizationRequestResolver implements ServerOAuth2AuthorizationRequestResolver { @@ -78,14 +79,18 @@ public class DefaultServerOAuth2AuthorizationRequestResolver implements ServerOA private static final char PATH_DELIMITER = '/'; - private final ServerWebExchangeMatcher authorizationRequestMatcher; + private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder()); - private final ReactiveClientRegistrationRepository clientRegistrationRepository; + private static final StringKeyGenerator DEFAULT_SECURE_KEY_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder().withoutPadding(), 96); - private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private static final Consumer DEFAULT_PKCE_APPLIER = OAuth2AuthorizationRequestCustomizers + .withPkce(); - private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator( - Base64.getUrlEncoder().withoutPadding(), 96); + private final ServerWebExchangeMatcher authorizationRequestMatcher; + + private final ReactiveClientRegistrationRepository clientRegistrationRepository; private Consumer authorizationRequestCustomizer = (customizer) -> { }; @@ -133,7 +138,7 @@ public Mono resolve(ServerWebExchange exchange) { @Override public Mono resolve(ServerWebExchange exchange, String clientRegistrationId) { - return this.findByRegistrationId(exchange, clientRegistrationId) + return findByRegistrationId(exchange, clientRegistrationId) .map((clientRegistration) -> authorizationRequest(exchange, clientRegistration)); } @@ -143,6 +148,7 @@ public Mono resolve(ServerWebExchange exchange, Stri * @param authorizationRequestCustomizer the {@code Consumer} to be provided the * {@link OAuth2AuthorizationRequest.Builder} * @since 5.3 + * @see OAuth2AuthorizationRequestCustomizers */ public final void setAuthorizationRequestCustomizer( Consumer authorizationRequestCustomizer) { @@ -159,17 +165,14 @@ private Mono findByRegistrationId(ServerWebExchange exchange private OAuth2AuthorizationRequest authorizationRequest(ServerWebExchange exchange, ClientRegistration clientRegistration) { + OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration); String redirectUriStr = expandRedirectUri(exchange.getRequest(), clientRegistration); - Map attributes = new HashMap<>(); - attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); - OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration, attributes); // @formatter:off builder.clientId(clientRegistration.getClientId()) .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) .redirectUri(redirectUriStr) .scopes(clientRegistration.getScopes()) - .state(this.stateGenerator.generateKey()) - .attributes(attributes); + .state(DEFAULT_STATE_GENERATOR.generateKey()); // @formatter:on this.authorizationRequestCustomizer.accept(builder); @@ -177,11 +180,13 @@ private OAuth2AuthorizationRequest authorizationRequest(ServerWebExchange exchan return builder.build(); } - private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration, - Map attributes) { + private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration) { if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { - OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode(); - Map additionalParameters = new HashMap<>(); + // @formatter:off + OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode() + .attributes((attrs) -> + attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + // @formatter:on if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && clientRegistration.getScopes().contains(OidcScopes.OPENID)) { // Section 3.1.2.1 Authentication Request - @@ -189,12 +194,11 @@ private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientR // scope // REQUIRED. OpenID Connect requests MUST contain the "openid" scope // value. - addNonceParameters(attributes, additionalParameters); + applyNonce(builder); } if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { - addPkceParameters(attributes, additionalParameters); + DEFAULT_PKCE_APPLIER.accept(builder); } - builder.additionalParameters(additionalParameters); return builder; } if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { @@ -261,54 +265,22 @@ private static String expandRedirectUri(ServerHttpRequest request, ClientRegistr /** * Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests. - * @param attributes where the {@link OidcParameterNames#NONCE} is stored for the - * authentication request - * @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is - * added for the authentication request + * @param builder where the {@link OidcParameterNames#NONCE} and hash is stored for + * the authentication request * * @since 5.2 * @see 3.1.2.1. * Authentication Request */ - private void addNonceParameters(Map attributes, Map additionalParameters) { + private static void applyNonce(OAuth2AuthorizationRequest.Builder builder) { try { - String nonce = this.secureKeyGenerator.generateKey(); + String nonce = DEFAULT_SECURE_KEY_GENERATOR.generateKey(); String nonceHash = createHash(nonce); - attributes.put(OidcParameterNames.NONCE, nonce); - additionalParameters.put(OidcParameterNames.NONCE, nonceHash); - } - catch (NoSuchAlgorithmException ex) { - } - } - - /** - * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization - * and Access Token Requests - * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the - * token request - * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, - * usually, {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in - * the authorization request. - * - * @since 5.2 - * @see 1.1. - * Protocol Flow - * @see 4.1. - * Client Creates a Code Verifier - * @see 4.2. - * Client Creates the Code Challenge - */ - private void addPkceParameters(Map attributes, Map additionalParameters) { - String codeVerifier = this.secureKeyGenerator.generateKey(); - attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); - try { - String codeChallenge = createHash(codeVerifier); - additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); - additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + builder.attributes((attrs) -> attrs.put(OidcParameterNames.NONCE, nonce)); + builder.additionalParameters((params) -> params.put(OidcParameterNames.NONCE, nonceHash)); } catch (NoSuchAlgorithmException ex) { - additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java index 0ea5e255527..49d8dc416e1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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,6 +18,7 @@ import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +43,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link JwtBearerOAuth2AuthorizedClientProvider}. @@ -87,6 +89,13 @@ public void setAccessTokenResponseClientWhenClientIsNullThenThrowIllegalArgument .withMessage("accessTokenResponseClient cannot be null"); } + @Test + public void setJwtAssertionResolverWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.authorizedClientProvider.setJwtAssertionResolver(null)) + .withMessage("jwtAssertionResolver cannot be null"); + } + @Test public void setClockSkewWhenNullThenThrowIllegalArgumentException() { // @formatter:off @@ -198,7 +207,7 @@ public void authorizeWhenJwtBearerAndTokenNotExpiredButClockSkewForcesExpiryThen } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalNotJwtThenUnableToAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtDoesNotResolveThenUnableToAuthorize() { // @formatter:off OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext .withClientRegistration(this.clientRegistration) @@ -209,7 +218,7 @@ public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalNotJwtThenUnableTo } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtResolvesThenAuthorize() { OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); // @formatter:off @@ -224,4 +233,25 @@ public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); } + @Test + public void authorizeWhenCustomJwtAssertionResolverSetThenUsed() { + Function jwtAssertionResolver = mock(Function.class); + given(jwtAssertionResolver.apply(any())).willReturn(this.jwtAssertion); + this.authorizedClientProvider.setJwtAssertionResolver(jwtAssertionResolver); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); + // @formatter:off + TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password"); + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withClientRegistration(this.clientRegistration) + .principal(principal) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext); + verify(jwtAssertionResolver).apply(any()); + assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(principal.getName()); + assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + } + } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java index 33279c6f947..2ec6e2f4a09 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,6 +19,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -93,6 +94,13 @@ public void setAccessTokenResponseClientWhenClientIsNullThenThrowIllegalArgument .withMessage("accessTokenResponseClient cannot be null"); } + @Test + public void setJwtAssertionResolverWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.authorizedClientProvider.setJwtAssertionResolver(null)) + .withMessage("jwtAssertionResolver cannot be null"); + } + @Test public void setClockSkewWhenNullThenThrowIllegalArgumentException() { // @formatter:off @@ -222,7 +230,7 @@ public void authorizeWhenJwtBearerAndTokenNotExpiredButClockSkewForcesExpiryThen } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalNotJwtThenUnableToAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtDoesNotResolveThenUnableToAuthorize() { // @formatter:off OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext .withClientRegistration(this.clientRegistration) @@ -251,7 +259,7 @@ public void authorizeWhenInvalidRequestThenThrowClientAuthorizationException() { } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtResolvesThenAuthorize() { OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(Mono.just(accessTokenResponse)); // @formatter:off @@ -266,4 +274,25 @@ public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); } + @Test + public void authorizeWhenCustomJwtAssertionResolverSetThenUsed() { + Function> jwtAssertionResolver = mock(Function.class); + given(jwtAssertionResolver.apply(any())).willReturn(Mono.just(this.jwtAssertion)); + this.authorizedClientProvider.setJwtAssertionResolver(jwtAssertionResolver); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(Mono.just(accessTokenResponse)); + // @formatter:off + TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password"); + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withClientRegistration(this.clientRegistration) + .principal(principal) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext).block(); + verify(jwtAssertionResolver).apply(any()); + assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(principal.getName()); + assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + } + } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java index d916424582b..4770fe3b2e5 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -83,6 +83,12 @@ public void convertWhenAuthorizationGrantRequestNullThenThrowIllegalArgumentExce .withMessage("authorizationGrantRequest cannot be null"); } + @Test + public void setJwtClientAssertionCustomizerWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setJwtClientAssertionCustomizer(null)) + .withMessage("jwtClientAssertionCustomizer cannot be null"); + } + @Test public void convertWhenOtherClientAuthenticationMethodThenNotCustomized() { // @formatter:off @@ -179,6 +185,51 @@ public void convertWhenClientSecretJwtClientAuthenticationMethodThenCustomized() assertThat(jws.getExpiresAt()).isNotNull(); } + @Test + public void convertWhenJwtClientAssertionCustomizerSetThenUsed() { + OctetSequenceKey secretJwk = TestJwks.DEFAULT_SECRET_JWK; + given(this.jwkResolver.apply(any())).willReturn(secretJwk); + + String headerName = "custom-header"; + String headerValue = "header-value"; + String claimName = "custom-claim"; + String claimValue = "claim-value"; + this.converter.setJwtClientAssertionCustomizer((context) -> { + context.getHeaders().header(headerName, headerValue); + context.getClaims().claim(claimName, claimValue); + }); + + // @formatter:off + ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials() + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .build(); + // @formatter:on + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest( + clientRegistration); + MultiValueMap parameters = this.converter.convert(clientCredentialsGrantRequest); + + assertThat(parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE)) + .isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + String encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION); + assertThat(encodedJws).isNotNull(); + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretJwk.toSecretKey()).build(); + Jwt jws = jwtDecoder.decode(encodedJws); + + assertThat(jws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(MacAlgorithm.HS256.getName()); + assertThat(jws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(secretJwk.getKeyID()); + assertThat(jws.getHeaders().get(headerName)).isEqualTo(headerValue); + assertThat(jws.getClaim(JwtClaimNames.ISS)).isEqualTo(clientRegistration.getClientId()); + assertThat(jws.getSubject()).isEqualTo(clientRegistration.getClientId()); + assertThat(jws.getAudience()) + .isEqualTo(Collections.singletonList(clientRegistration.getProviderDetails().getTokenUri())); + assertThat(jws.getId()).isNotNull(); + assertThat(jws.getIssuedAt()).isNotNull(); + assertThat(jws.getExpiresAt()).isNotNull(); + assertThat(jws.getClaimAsString(claimName)).isEqualTo(claimValue); + } + // gh-9814 @Test public void convertWhenClientKeyChangesThenNewKeyUsed() throws Exception { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index 1502f517de8..444f83de60a 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -191,83 +191,6 @@ private void configureJwtClientAuthenticationConverter(Function oauth2ErrorConverter = mock(HttpMessageConverter.class); + this.errorHandler.setErrorConverter(oauth2ErrorConverter); + // @formatter:off + String errorResponse = "{\n" + + " \"errorCode\": \"unauthorized_client\",\n" + + " \"errorSummary\": \"The client is not authorized\"\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse(errorResponse.getBytes(), HttpStatus.BAD_REQUEST); + given(oauth2ErrorConverter.read(any(), any())) + .willReturn(new OAuth2Error("unauthorized_client", "The client is not authorized", null)); + + assertThatExceptionOfType(OAuth2AuthorizationException.class) + .isThrownBy(() -> this.errorHandler.handleError(response)) + .withMessage("[unauthorized_client] The client is not authorized"); + verify(oauth2ErrorConverter).read(eq(OAuth2Error.class), eq(response)); + } + @Test public void handleErrorWhenErrorResponseWwwAuthenticateHeaderThenHandled() { String wwwAuthenticateHeader = "Bearer realm=\"auth-realm\" error=\"insufficient_scope\" error_description=\"The access token expired\""; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java index b5ca968f99a..c2bd1152c97 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java @@ -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. @@ -138,6 +138,22 @@ public void logoutWhenUsingPostLogoutRedirectUriTemplateThenBuildsItForRedirect( "https://endpoint?" + "id_token_hint=id-token&" + "post_logout_redirect_uri=https://rp.example.org"); } + @Test + public void logoutWhenUsingPostLogoutRedirectUriTemplateThenBuildsItForRedirectExpanded() + throws IOException, ServletException { + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(TestOidcUsers.create(), + AuthorityUtils.NO_AUTHORITIES, this.registration.getRegistrationId()); + this.handler.setPostLogoutRedirectUri("{baseUrl}/{registrationId}"); + this.request.setScheme("https"); + this.request.setServerPort(443); + this.request.setServerName("rp.example.org"); + this.request.setUserPrincipal(token); + this.handler.onLogoutSuccess(this.request, this.response, token); + assertThat(this.response.getRedirectedUrl()).isEqualTo(String.format( + "https://endpoint?" + "id_token_hint=id-token&" + "post_logout_redirect_uri=https://rp.example.org/%s", + this.registration.getRegistrationId())); + } + // gh-9511 @Test public void logoutWhenUsingPostLogoutRedirectUriWithQueryParametersThenBuildsItForRedirect() diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java index 5c91205e4a7..0345f55a29f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -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. @@ -57,7 +57,7 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { private ClientRegistration fineRedirectUriTemplateRegistration; - private ClientRegistration pkceRegistration; + private ClientRegistration publicClientRegistration; private ClientRegistration oidcRegistration; @@ -73,9 +73,9 @@ public void setUp() { this.registration2 = TestClientRegistrations.clientRegistration2().build(); this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build(); // @formatter:off - this.pkceRegistration = TestClientRegistrations.clientRegistration() - .registrationId("pkce-client-registration-id") - .clientId("pkce-client-id") + this.publicClientRegistration = TestClientRegistrations.clientRegistration() + .registrationId("public-client-registration-id") + .clientId("public-client-id") .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) .clientSecret(null) .build(); @@ -85,7 +85,7 @@ public void setUp() { .build(); // @formatter:on this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1, - this.registration2, this.fineRedirectUriTemplateRegistration, this.pkceRegistration, + this.registration2, this.fineRedirectUriTemplateRegistration, this.publicClientRegistration, this.oidcRegistration); this.resolver = new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository, this.authorizationRequestBaseUri); @@ -371,8 +371,8 @@ public void resolveWhenAuthorizationRequestHasActionParameterLoginThenRedirectUr } @Test - public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() { - ClientRegistration clientRegistration = this.pkceRegistration; + public void resolveWhenAuthorizationRequestWithValidPublicClientThenResolves() { + ClientRegistration clientRegistration = this.publicClientRegistration; String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); @@ -398,10 +398,84 @@ public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() { assertThat((String) authorizationRequest.getAttribute(PkceParameterNames.CODE_VERIFIER)) .matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$"); assertThat(authorizationRequest.getAuthorizationRequestUri()) - .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=pkce-client-id&" - + "scope=read:user&state=.{15,}&" - + "redirect_uri=http://localhost/login/oauth2/code/pkce-client-registration-id&" - + "code_challenge_method=S256&" + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); + .matches("https://example.com/login/oauth/authorize\\?" + + "response_type=code&client_id=public-client-id&" + "scope=read:user&state=.{15,}&" + + "redirect_uri=http://localhost/login/oauth2/code/public-client-registration-id&" + + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "code_challenge_method=S256"); + } + + // gh-6548 + @Test + public void resolveWhenAuthorizationRequestApplyPkceToConfidentialClientsThenApplied() { + this.resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); + + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertPkceApplied(authorizationRequest, clientRegistration); + + clientRegistration = this.registration2; + requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + authorizationRequest = this.resolver.resolve(request); + assertPkceApplied(authorizationRequest, clientRegistration); + } + + // gh-6548 + @Test + public void resolveWhenAuthorizationRequestApplyPkceToSpecificConfidentialClientThenApplied() { + this.resolver.setAuthorizationRequestCustomizer((builder) -> { + builder.attributes((attrs) -> { + String registrationId = (String) attrs.get(OAuth2ParameterNames.REGISTRATION_ID); + if (this.registration1.getRegistrationId().equals(registrationId)) { + OAuth2AuthorizationRequestCustomizers.withPkce().accept(builder); + } + }); + }); + + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertPkceApplied(authorizationRequest, clientRegistration); + + clientRegistration = this.registration2; + requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + authorizationRequest = this.resolver.resolve(request); + assertPkceNotApplied(authorizationRequest, clientRegistration); + } + + private void assertPkceApplied(OAuth2AuthorizationRequest authorizationRequest, + ClientRegistration clientRegistration) { + assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE); + assertThat(authorizationRequest.getAdditionalParameters()) + .contains(entry(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")); + assertThat(authorizationRequest.getAttributes()).containsKey(PkceParameterNames.CODE_VERIFIER); + assertThat((String) authorizationRequest.getAttribute(PkceParameterNames.CODE_VERIFIER)) + .matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$"); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&" + "client_id=" + + clientRegistration.getClientId() + "&" + "scope=read:user&" + "state=.{15,}&" + + "redirect_uri=http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId() + + "&" + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "code_challenge_method=S256"); + } + + private void assertPkceNotApplied(OAuth2AuthorizationRequest authorizationRequest, + ClientRegistration clientRegistration) { + assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(PkceParameterNames.CODE_CHALLENGE); + assertThat(authorizationRequest.getAdditionalParameters()) + .doesNotContainKey(PkceParameterNames.CODE_CHALLENGE_METHOD); + assertThat(authorizationRequest.getAttributes()).doesNotContainKey(PkceParameterNames.CODE_VERIFIER); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&" + "client_id=" + + clientRegistration.getClientId() + "&" + "scope=read:user&" + "state=.{15,}&" + + "redirect_uri=http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); } @Test @@ -444,7 +518,7 @@ public void resolveWhenAuthorizationRequestCustomizerRemovesNonceThenQueryExclud MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); this.resolver.setAuthorizationRequestCustomizer( - (customizer) -> customizer.additionalParameters((params) -> params.remove(OidcParameterNames.NONCE)) + (builder) -> builder.additionalParameters((params) -> params.remove(OidcParameterNames.NONCE)) .attributes((attrs) -> attrs.remove(OidcParameterNames.NONCE))); OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OidcParameterNames.NONCE); @@ -462,11 +536,10 @@ public void resolveWhenAuthorizationRequestCustomizerAddsParameterThenQueryInclu String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); - this.resolver - .setAuthorizationRequestCustomizer((customizer) -> customizer.authorizationRequestUri((uriBuilder) -> { - uriBuilder.queryParam("param1", "value1"); - return uriBuilder.build(); - })); + this.resolver.setAuthorizationRequestCustomizer((builder) -> builder.authorizationRequestUri((uriBuilder) -> { + uriBuilder.queryParam("param1", "value1"); + return uriBuilder.build(); + })); OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); assertThat(authorizationRequest.getAuthorizationRequestUri()) .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" @@ -481,7 +554,7 @@ public void resolveWhenAuthorizationRequestCustomizerOverridesParameterThenQuery String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); - this.resolver.setAuthorizationRequestCustomizer((customizer) -> customizer.parameters((params) -> { + this.resolver.setAuthorizationRequestCustomizer((builder) -> builder.parameters((params) -> { params.put("appid", params.get("client_id")); params.remove("client_id"); })); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java index 0762eba0b62..464d641c96a 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java @@ -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. @@ -29,6 +29,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -41,7 +42,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; /** @@ -106,7 +109,7 @@ public void resolveWhenForwardedHeadersClientRegistrationFoundThenWorks() { } @Test - public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() { + public void resolveWhenAuthorizationRequestWithValidPublicClientThenResolves() { given(this.clientRegistrationRepository.findByRegistrationId(any())) .willReturn(Mono.just(TestClientRegistrations.clientRegistration() .clientAuthenticationMethod(ClientAuthenticationMethod.NONE).clientSecret(null).build())); @@ -116,7 +119,79 @@ public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() { assertThat(request.getAuthorizationRequestUri()) .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.*?&" + "redirect_uri=/login/oauth2/code/registration-id&" - + "code_challenge_method=S256&" + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); + + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "code_challenge_method=S256"); + } + + // gh-6548 + @Test + public void resolveWhenAuthorizationRequestApplyPkceToConfidentialClientsThenApplied() { + ClientRegistration registration1 = TestClientRegistrations.clientRegistration().build(); + given(this.clientRegistrationRepository.findByRegistrationId(eq(registration1.getRegistrationId()))) + .willReturn(Mono.just(registration1)); + ClientRegistration registration2 = TestClientRegistrations.clientRegistration2().build(); + given(this.clientRegistrationRepository.findByRegistrationId(eq(registration2.getRegistrationId()))) + .willReturn(Mono.just(registration2)); + + this.resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); + + OAuth2AuthorizationRequest request = resolve("/oauth2/authorization/" + registration1.getRegistrationId()); + assertPkceApplied(request, registration1); + + request = resolve("/oauth2/authorization/" + registration2.getRegistrationId()); + assertPkceApplied(request, registration2); + } + + // gh-6548 + @Test + public void resolveWhenAuthorizationRequestApplyPkceToSpecificConfidentialClientThenApplied() { + ClientRegistration registration1 = TestClientRegistrations.clientRegistration().build(); + given(this.clientRegistrationRepository.findByRegistrationId(eq(registration1.getRegistrationId()))) + .willReturn(Mono.just(registration1)); + ClientRegistration registration2 = TestClientRegistrations.clientRegistration2().build(); + given(this.clientRegistrationRepository.findByRegistrationId(eq(registration2.getRegistrationId()))) + .willReturn(Mono.just(registration2)); + + this.resolver.setAuthorizationRequestCustomizer((builder) -> { + builder.attributes((attrs) -> { + String registrationId = (String) attrs.get(OAuth2ParameterNames.REGISTRATION_ID); + if (registration1.getRegistrationId().equals(registrationId)) { + OAuth2AuthorizationRequestCustomizers.withPkce().accept(builder); + } + }); + }); + + OAuth2AuthorizationRequest request = resolve("/oauth2/authorization/" + registration1.getRegistrationId()); + assertPkceApplied(request, registration1); + + request = resolve("/oauth2/authorization/" + registration2.getRegistrationId()); + assertPkceNotApplied(request, registration2); + } + + private void assertPkceApplied(OAuth2AuthorizationRequest authorizationRequest, + ClientRegistration clientRegistration) { + assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE); + assertThat(authorizationRequest.getAdditionalParameters()) + .contains(entry(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")); + assertThat(authorizationRequest.getAttributes()).containsKey(PkceParameterNames.CODE_VERIFIER); + assertThat((String) authorizationRequest.getAttribute(PkceParameterNames.CODE_VERIFIER)) + .matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$"); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&" + "client_id=" + + clientRegistration.getClientId() + "&" + "scope=read:user&" + "state=.{15,}&" + + "redirect_uri=/login/oauth2/code/" + clientRegistration.getRegistrationId() + "&" + + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "code_challenge_method=S256"); + } + + private void assertPkceNotApplied(OAuth2AuthorizationRequest authorizationRequest, + ClientRegistration clientRegistration) { + assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(PkceParameterNames.CODE_CHALLENGE); + assertThat(authorizationRequest.getAdditionalParameters()) + .doesNotContainKey(PkceParameterNames.CODE_CHALLENGE_METHOD); + assertThat(authorizationRequest.getAttributes()).doesNotContainKey(PkceParameterNames.CODE_VERIFIER); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&" + "client_id=" + + clientRegistration.getClientId() + "&" + "scope=read:user&" + "state=.{15,}&" + + "redirect_uri=/login/oauth2/code/" + clientRegistration.getRegistrationId()); } @Test @@ -136,7 +211,7 @@ public void resolveWhenAuthorizationRequestCustomizerRemovesNonceThenQueryExclud given(this.clientRegistrationRepository.findByRegistrationId(any())) .willReturn(Mono.just(TestClientRegistrations.clientRegistration().scope(OidcScopes.OPENID).build())); this.resolver.setAuthorizationRequestCustomizer( - (customizer) -> customizer.additionalParameters((params) -> params.remove(OidcParameterNames.NONCE)) + (builder) -> builder.additionalParameters((params) -> params.remove(OidcParameterNames.NONCE)) .attributes((attrs) -> attrs.remove(OidcParameterNames.NONCE))); OAuth2AuthorizationRequest authorizationRequest = resolve("/oauth2/authorization/registration-id"); assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OidcParameterNames.NONCE); @@ -151,11 +226,10 @@ public void resolveWhenAuthorizationRequestCustomizerRemovesNonceThenQueryExclud public void resolveWhenAuthorizationRequestCustomizerAddsParameterThenQueryIncludesParameter() { given(this.clientRegistrationRepository.findByRegistrationId(any())) .willReturn(Mono.just(TestClientRegistrations.clientRegistration().scope(OidcScopes.OPENID).build())); - this.resolver - .setAuthorizationRequestCustomizer((customizer) -> customizer.authorizationRequestUri((uriBuilder) -> { - uriBuilder.queryParam("param1", "value1"); - return uriBuilder.build(); - })); + this.resolver.setAuthorizationRequestCustomizer((builder) -> builder.authorizationRequestUri((uriBuilder) -> { + uriBuilder.queryParam("param1", "value1"); + return uriBuilder.build(); + })); OAuth2AuthorizationRequest authorizationRequest = resolve("/oauth2/authorization/registration-id"); assertThat(authorizationRequest.getAuthorizationRequestUri()) .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" @@ -167,7 +241,7 @@ public void resolveWhenAuthorizationRequestCustomizerAddsParameterThenQueryInclu public void resolveWhenAuthorizationRequestCustomizerOverridesParameterThenQueryIncludesParameter() { given(this.clientRegistrationRepository.findByRegistrationId(any())) .willReturn(Mono.just(TestClientRegistrations.clientRegistration().scope(OidcScopes.OPENID).build())); - this.resolver.setAuthorizationRequestCustomizer((customizer) -> customizer.parameters((params) -> { + this.resolver.setAuthorizationRequestCustomizer((builder) -> builder.parameters((params) -> { params.put("appid", params.get("client_id")); params.remove("client_id"); })); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcUserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcUserInfo.java index 812a03ddd7b..55126dedc22 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcUserInfo.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcUserInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -248,11 +248,23 @@ public Builder phoneNumber(String phoneNumber) { * Use this verified-phone-number indicator in the resulting {@link OidcUserInfo} * @param phoneNumberVerified The verified-phone-number indicator to use * @return the {@link Builder} for further configurations + * @deprecated Use {@link Builder#phoneNumberVerified(Boolean)} */ + @Deprecated public Builder phoneNumberVerified(String phoneNumberVerified) { return this.claim(StandardClaimNames.PHONE_NUMBER_VERIFIED, phoneNumberVerified); } + /** + * Use this verified-phone-number indicator in the resulting {@link OidcUserInfo} + * @param phoneNumberVerified The verified-phone-number indicator to use + * @return the {@link Builder} for further configurations + * @since 5.8 + */ + public Builder phoneNumberVerified(Boolean phoneNumberVerified) { + return this.claim(StandardClaimNames.PHONE_NUMBER_VERIFIED, phoneNumberVerified); + } + /** * Use this preferred username in the resulting {@link OidcUserInfo} * @param preferredUsername The preferred username to use diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java index 73c13c7dc29..0202815cc20 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java @@ -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 JwtClaimValidator(String claim, Predicate test) { 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"); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java index 430f7078924..a43989c8680 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java @@ -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 void validateWhenClaimPassesTheTestThenReturnsSuccess() { @Test public void validateWhenClaimFailsTheTestThenReturnsFailure() { Jwt jwt = TestJwts.jwt().claim(JwtClaimNames.ISS, "http://abc").build(); + Collection 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 diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java index 7f8a093ad3c..72164cf21b7 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -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.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 void validateWhenJwtIsExpiredThenErrorMessageIndicatesExpirationTime() { .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 void validateWhenJwtIsTooEarlyThenErrorMessageIndicatesNotBeforeTime() { .collect(Collectors.toList()); // @formatter:on assertThat(messages).contains("Jwt used before " + oneHourFromNow); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle index 438bbc8b5d5..69e705766b2 100644 --- a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -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' diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java index afbfb38f9c4..26aeca6d085 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.resource.authentication; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -186,7 +187,7 @@ public Mono resolve(String issuer) { return this.authenticationManagers.computeIfAbsent(issuer, (k) -> Mono.fromCallable(() -> new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k))) .subscribeOn(Schedulers.boundedElastic()) - .cache() + .cache((manager) -> Duration.ofMillis(Long.MAX_VALUE), (ex) -> Duration.ZERO, () -> Duration.ZERO) ); // @formatter:on } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java index ffa9701ad39..fb89e4b4f28 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java @@ -38,6 +38,8 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; @@ -75,6 +77,8 @@ public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + /** * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) * @param authenticationManagerResolver @@ -131,6 +135,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authenticationResult); SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authenticationResult)); } @@ -143,6 +148,18 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + /** * Set the {@link BearerTokenResolver} to use. Defaults to * {@link DefaultBearerTokenResolver}. diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java index 99ab3933b99..8bc9573eda7 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java @@ -96,6 +96,40 @@ public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() thro } } + @Test + public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(500) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(JWK_SET) + ); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + issuer); + Authentication token = withBearerToken(jws.serialize()); + AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null); + assertThat(authenticationManager).isNotNull(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> authenticationManager.authenticate(token)); + Authentication authentication = authenticationManager.authenticate(token); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + @Test public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception { try (MockWebServer server = new MockWebServer()) { diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java index c13eac86f86..357d95423d6 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java @@ -95,6 +95,34 @@ public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() thro } } + // gh-10444 + @Test + public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(500).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + issuer); + ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block(); + assertThat(authenticationManager).isNotNull(); + Authentication token = withBearerToken(jws.serialize()); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> authenticationManager.authenticate(token).block()); + Authentication authentication = authenticationManager.authenticate(token).block(); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + @Test public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception { try (MockWebServer server = new MockWebServer()) { diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java index 418ca85c0a1..5f423774108 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java @@ -36,18 +36,23 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.BearerTokenError; import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.context.SecurityContextRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -102,6 +107,26 @@ public void doFilterWhenBearerTokenPresentThenAuthenticates() throws ServletExce assertThat(captor.getValue().getPrincipal()).isEqualTo("token"); } + @Test + public void doFilterWhenSecurityContextRepositoryThenSaves() throws ServletException, IOException { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + String token = "token"; + given(this.bearerTokenResolver.resolve(this.request)).willReturn(token); + TestingAuthenticationToken expectedAuthentication = new TestingAuthenticationToken("test", "password"); + given(this.authenticationManager.authenticate(any())).willReturn(expectedAuthentication); + BearerTokenAuthenticationFilter filter = addMocks( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + filter.setSecurityContextRepository(securityContextRepository); + filter.doFilter(this.request, this.response, this.filterChain); + ArgumentCaptor captor = ArgumentCaptor + .forClass(BearerTokenAuthenticationToken.class); + verify(this.authenticationManager).authenticate(captor.capture()); + assertThat(captor.getValue().getPrincipal()).isEqualTo(token); + ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); + verify(securityContextRepository).saveContext(contextArg.capture(), eq(this.request), eq(this.response)); + assertThat(contextArg.getValue().getAuthentication().getName()).isEqualTo(expectedAuthentication.getName()); + } + @Test public void doFilterWhenUsingAuthenticationManagerResolverThenAuthenticates() throws Exception { BearerTokenAuthenticationFilter filter = addMocks( diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index 0725b0d0a8b..5bbd99d35b7 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -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" diff --git a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java index 2bc034861d4..8a90e415974 100644 --- a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java +++ b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java @@ -162,7 +162,8 @@ public void testDoesntSupport() { public void testIgnoresUserPassAuthToken() { OpenIDAuthenticationProvider provider = new OpenIDAuthenticationProvider(); provider.setUserDetailsService(new MockUserDetailsService()); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(USERNAME, "password"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(USERNAME, + "password"); assertThat(provider.authenticate(token)).isNull(); } diff --git a/remoting/src/main/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocation.java b/remoting/src/main/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocation.java index a6f526909ed..078d819823d 100644 --- a/remoting/src/main/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocation.java +++ b/remoting/src/main/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocation.java @@ -118,7 +118,7 @@ public Object invoke(Object targetObject) * Creates the server-side authentication request object. */ protected Authentication createAuthenticationRequest(String principal, String credentials) { - return new UsernamePasswordAuthenticationToken(principal, credentials); + return UsernamePasswordAuthenticationToken.unauthenticated(principal, credentials); } } diff --git a/remoting/src/test/java/org/springframework/security/remoting/httpinvoker/AuthenticationSimpleHttpInvokerRequestExecutorTests.java b/remoting/src/test/java/org/springframework/security/remoting/httpinvoker/AuthenticationSimpleHttpInvokerRequestExecutorTests.java index 859ded84727..ef0df2cf8c1 100644 --- a/remoting/src/test/java/org/springframework/security/remoting/httpinvoker/AuthenticationSimpleHttpInvokerRequestExecutorTests.java +++ b/remoting/src/test/java/org/springframework/security/remoting/httpinvoker/AuthenticationSimpleHttpInvokerRequestExecutorTests.java @@ -48,7 +48,8 @@ public void tearDown() { @Test public void testNormalOperation() throws Exception { // Setup client-side context - Authentication clientSideAuthentication = new UsernamePasswordAuthenticationToken("Aladdin", "open sesame"); + Authentication clientSideAuthentication = UsernamePasswordAuthenticationToken.unauthenticated("Aladdin", + "open sesame"); SecurityContextHolder.getContext().setAuthentication(clientSideAuthentication); // Create a connection and ensure our executor sets its // properties correctly diff --git a/remoting/src/test/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocationTests.java b/remoting/src/test/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocationTests.java index ed37bc00a9e..facd4cea0f4 100644 --- a/remoting/src/test/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocationTests.java +++ b/remoting/src/test/java/org/springframework/security/remoting/rmi/ContextPropagatingRemoteInvocationTests.java @@ -56,7 +56,7 @@ private ContextPropagatingRemoteInvocation getRemoteInvocation() throws Exceptio @Test public void testContextIsResetEvenIfExceptionOccurs() throws Exception { // Setup client-side context - Authentication clientSideAuthentication = new UsernamePasswordAuthenticationToken("rod", "koala"); + Authentication clientSideAuthentication = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); SecurityContextHolder.getContext().setAuthentication(clientSideAuthentication); ContextPropagatingRemoteInvocation remoteInvocation = getRemoteInvocation(); // Set up the wrong arguments. @@ -70,7 +70,7 @@ public void testContextIsResetEvenIfExceptionOccurs() throws Exception { @Test public void testNormalOperation() throws Exception { // Setup client-side context - Authentication clientSideAuthentication = new UsernamePasswordAuthenticationToken("rod", "koala"); + Authentication clientSideAuthentication = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); SecurityContextHolder.getContext().setAuthentication(clientSideAuthentication); ContextPropagatingRemoteInvocation remoteInvocation = getRemoteInvocation(); // Set to null, as ContextPropagatingRemoteInvocation already obtained @@ -95,7 +95,7 @@ public void testNullContextHolderDoesNotCauseInvocationProblems() throws Excepti // SEC-1867 @Test public void testNullCredentials() throws Exception { - Authentication clientSideAuthentication = new UsernamePasswordAuthenticationToken("rod", null); + Authentication clientSideAuthentication = UsernamePasswordAuthenticationToken.unauthenticated("rod", null); SecurityContextHolder.getContext().setAuthentication(clientSideAuthentication); ContextPropagatingRemoteInvocation remoteInvocation = getRemoteInvocation(); assertThat(ReflectionTestUtils.getField(remoteInvocation, "credentials")).isNull(); diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java b/rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java index b804004ae58..bbe90e5eb83 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java +++ b/rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-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. @@ -96,7 +96,7 @@ private Authentication simple(ByteBuf rawAuthentication) { String username = rawUsername.toString(StandardCharsets.UTF_8); ByteBuf rawPassword = AuthMetadataCodec.readPassword(rawAuthentication); String password = rawPassword.toString(StandardCharsets.UTF_8); - return new UsernamePasswordAuthenticationToken(username, password); + return UsernamePasswordAuthenticationToken.unauthenticated(username, password); } private Authentication bearer(ByteBuf rawAuthentication) { diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/authentication/BasicAuthenticationPayloadExchangeConverter.java b/rsocket/src/main/java/org/springframework/security/rsocket/authentication/BasicAuthenticationPayloadExchangeConverter.java index 1a806c3bb80..0d3a9cc76d9 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/authentication/BasicAuthenticationPayloadExchangeConverter.java +++ b/rsocket/src/main/java/org/springframework/security/rsocket/authentication/BasicAuthenticationPayloadExchangeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-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. @@ -49,9 +49,8 @@ public Mono convert(PayloadExchange exchange) { return Mono.fromCallable(() -> this.metadataExtractor.extract(exchange.getPayload(), this.metadataMimetype)) .flatMap((metadata) -> Mono .justOrEmpty(metadata.get(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE.toString()))) - .cast(UsernamePasswordMetadata.class) - .map((credentials) -> new UsernamePasswordAuthenticationToken(credentials.getUsername(), - credentials.getPassword())); + .cast(UsernamePasswordMetadata.class).map((credentials) -> UsernamePasswordAuthenticationToken + .unauthenticated(credentials.getUsername(), credentials.getPassword())); } private static MetadataExtractor createDefaultExtractor() { diff --git a/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java b/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java index c622ef7cc8f..82495f2cb96 100644 --- a/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java +++ b/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-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. @@ -89,8 +89,8 @@ public void interceptWhenBasicCredentialsThenAuthenticates() { interceptor.intercept(exchange, authenticationPayloadChain).block(); Authentication authentication = authenticationPayloadChain.getAuthentication(); verify(this.authenticationManager).authenticate(this.authenticationArg.capture()); - assertThat(this.authenticationArg.getValue()) - .isEqualToComparingFieldByField(new UsernamePasswordAuthenticationToken("user", "password")); + assertThat(this.authenticationArg.getValue()).isEqualToComparingFieldByField( + UsernamePasswordAuthenticationToken.unauthenticated("user", "password")); assertThat(authentication).isEqualTo(expectedAuthentication); } diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index 5bcc457a617..c9cc0e6f684 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -36,10 +36,19 @@ configurations { } compileOpensaml4MainJava { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } sourceCompatibility = '11' targetCompatibility = '11' } +compileOpensaml4TestJava { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } +} + dependencies { management platform(project(":spring-security-dependencies")) api project(':spring-security-web') @@ -50,10 +59,13 @@ 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' + + optional 'com.fasterxml.jackson.core:jackson-databind' testImplementation 'com.squareup.okhttp3:mockwebserver' testImplementation "org.assertj:assertj-core" + testImplementation "org.skyscreamer:jsonassert" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" testImplementation "org.junit.jupiter:junit-jupiter-engine" diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java index b7f4d9c7994..ecee9777f1e 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -31,6 +31,13 @@ public interface Saml2ErrorCodes { */ String UNKNOWN_RESPONSE_CLASS = "unknown_response_class"; + /** + * The serialized AuthNRequest could not be deserialized correctly. + * + * @since 5.7 + */ + String MALFORMED_REQUEST_DATA = "malformed_request_data"; + /** * The response data is malformed or incomplete. An invalid XML object was received, * and XML unmarshalling failed. @@ -116,4 +123,11 @@ public interface Saml2ErrorCodes { */ String RELYING_PARTY_REGISTRATION_NOT_FOUND = "relying_party_registration_not_found"; + /** + * The InResponseTo content of the response does not match the ID of the AuthNRequest. + * + * @since 5.7 + */ + String INVALID_IN_RESPONSE_TO = "invalid_in_response_to"; + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java new file mode 100644 index 00000000000..ca909bcc925 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link DefaultSaml2AuthenticatedPrincipal}. + * + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new Saml2Jackson2Module());
      + * 
      + * + * @author Ulrich Grave + * @since 5.7 + * @see DefaultSaml2AuthenticatedPrincipalDeserializer + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +class DefaultSaml2AuthenticatedPrincipalMixin { + + @JsonProperty("registrationId") + String registrationId; + + DefaultSaml2AuthenticatedPrincipalMixin(@JsonProperty("name") String name, + @JsonProperty("attributes") Map> attributes, + @JsonProperty("sessionIndexes") List sessionIndexes) { + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationExceptionMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationExceptionMixin.java new file mode 100644 index 00000000000..3bbee42f240 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationExceptionMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; + +/** + * This mixin class is used to serialize/deserialize {@link Saml2AuthenticationException}. + * + * @author Ulrich Grave + * @since 5.7 + * @see Saml2AuthenticationException + * @see Saml2Jackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace", "suppressedExceptions" }) +class Saml2AuthenticationExceptionMixin { + + @JsonCreator + Saml2AuthenticationExceptionMixin(@JsonProperty("error") Saml2Error error, + @JsonProperty("detailMessage") String message) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java new file mode 100644 index 00000000000..d8d936e4dbb --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link Saml2Authentication}. + * + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new Saml2Jackson2Module());
      + * 
      + * + * @author Ulrich Grave + * @since 5.7 + * @see Saml2AuthenticationDeserializer + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true) +class Saml2AuthenticationMixin { + + @JsonCreator + Saml2AuthenticationMixin(@JsonProperty("principal") AuthenticatedPrincipal principal, + @JsonProperty("saml2Response") String saml2Response, + @JsonProperty("authorities") Collection authorities) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2ErrorMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2ErrorMixin.java new file mode 100644 index 00000000000..b4af0e70612 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2ErrorMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.saml2.core.Saml2Error; + +/** + * This mixin class is used to serialize/deserialize {@link Saml2Error}. + * + * @author Ulrich Grave + * @since 5.7 + * @see Saml2Error + * @see Saml2Jackson2Module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +class Saml2ErrorMixin { + + @JsonCreator + Saml2ErrorMixin(@JsonProperty("errorCode") String errorCode, @JsonProperty("description") String description) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java new file mode 100644 index 00000000000..025ffc6b36b --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; + +/** + * Jackson module for saml2-service-provider. This module register + * {@link Saml2AuthenticationMixin}, {@link DefaultSaml2AuthenticatedPrincipalMixin}, + * {@link Saml2LogoutRequestMixin}, {@link Saml2RedirectAuthenticationRequestMixin}, + * {@link Saml2PostAuthenticationRequestMixin}, {@link Saml2ErrorMixin} and + * {@link Saml2AuthenticationExceptionMixin}. + * + * @author Ulrich Grave + * @since 5.7 + * @see SecurityJackson2Modules + */ +public class Saml2Jackson2Module extends SimpleModule { + + public Saml2Jackson2Module() { + super(Saml2Jackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixInAnnotations(Saml2Authentication.class, Saml2AuthenticationMixin.class); + context.setMixInAnnotations(DefaultSaml2AuthenticatedPrincipal.class, + DefaultSaml2AuthenticatedPrincipalMixin.class); + context.setMixInAnnotations(Saml2LogoutRequest.class, Saml2LogoutRequestMixin.class); + context.setMixInAnnotations(Saml2RedirectAuthenticationRequest.class, + Saml2RedirectAuthenticationRequestMixin.class); + context.setMixInAnnotations(Saml2PostAuthenticationRequest.class, Saml2PostAuthenticationRequestMixin.class); + context.setMixInAnnotations(Saml2Error.class, Saml2ErrorMixin.class); + context.setMixInAnnotations(Saml2AuthenticationException.class, Saml2AuthenticationExceptionMixin.class); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java new file mode 100644 index 00000000000..4eb0440eba3 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import java.util.Map; +import java.util.function.Function; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link Saml2LogoutRequest}. + * + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new Saml2Jackson2Module());
      + * 
      + * + * @author Ulrich Grave + * @since 5.7 + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +class Saml2LogoutRequestMixin { + + @JsonIgnore + Function, String> encoder; + + @JsonCreator + Saml2LogoutRequestMixin(@JsonProperty("location") String location, + @JsonProperty("relayState") Saml2MessageBinding relayState, + @JsonProperty("parameters") Map parameters, @JsonProperty("id") String id, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java new file mode 100644 index 00000000000..53ddeb73d97 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link Saml2PostAuthenticationRequest}. + * + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new Saml2Jackson2Module());
      + * 
      + * + * @author Ulrich Grave + * @since 5.7 + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +class Saml2PostAuthenticationRequestMixin { + + @JsonCreator + Saml2PostAuthenticationRequestMixin(@JsonProperty("samlRequest") String samlRequest, + @JsonProperty("relayState") String relayState, + @JsonProperty("authenticationRequestUri") String authenticationRequestUri, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java new file mode 100644 index 00000000000..247b52104c5 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link Saml2RedirectAuthenticationRequest}. + * + *
      + *     ObjectMapper mapper = new ObjectMapper();
      + *     mapper.registerModule(new Saml2Jackson2Module());
      + * 
      + * + * @author Ulrich Grave + * @since 5.7 + * @see Saml2Jackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +class Saml2RedirectAuthenticationRequestMixin { + + @JsonCreator + Saml2RedirectAuthenticationRequestMixin(@JsonProperty("samlRequest") String samlRequest, + @JsonProperty("sigAlg") String sigAlg, @JsonProperty("signature") String signature, + @JsonProperty("relayState") String relayState, + @JsonProperty("authenticationRequestUri") String authenticationRequestUri, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java index 028ecd6bae1..f6f23c4c21a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java @@ -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. @@ -16,8 +16,11 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.io.Serializable; import java.nio.charset.Charset; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.util.Assert; @@ -34,7 +37,9 @@ * @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext) * @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext) */ -public abstract class AbstractSaml2AuthenticationRequest { +public abstract class AbstractSaml2AuthenticationRequest implements Serializable { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final String samlRequest; @@ -42,6 +47,8 @@ public abstract class AbstractSaml2AuthenticationRequest { private final String authenticationRequestUri; + private final String relyingPartyRegistrationId; + /** * Mandatory constructor for the {@link AbstractSaml2AuthenticationRequest} * @param samlRequest - the SAMLRequest XML data, SAML encoded, cannot be empty or @@ -49,13 +56,17 @@ public abstract class AbstractSaml2AuthenticationRequest { * @param relayState - RelayState value that accompanies the request, may be null * @param authenticationRequestUri - The authenticationRequestUri, a URL, where to * send the XML message, cannot be empty or null + * @param relyingPartyRegistrationId the registration id of the relying party, may be + * null */ - AbstractSaml2AuthenticationRequest(String samlRequest, String relayState, String authenticationRequestUri) { + AbstractSaml2AuthenticationRequest(String samlRequest, String relayState, String authenticationRequestUri, + String relyingPartyRegistrationId) { Assert.hasText(samlRequest, "samlRequest cannot be null or empty"); Assert.hasText(authenticationRequestUri, "authenticationRequestUri cannot be null or empty"); this.authenticationRequestUri = authenticationRequestUri; this.samlRequest = samlRequest; this.relayState = relayState; + this.relyingPartyRegistrationId = relyingPartyRegistrationId; } /** @@ -85,6 +96,16 @@ public String getAuthenticationRequestUri() { return this.authenticationRequestUri; } + /** + * The identifier for the {@link RelyingPartyRegistration} associated with this + * request + * @return the {@link RelyingPartyRegistration} id + * @since 5.8 + */ + public String getRelyingPartyRegistrationId() { + return this.relyingPartyRegistrationId; + } + /** * Returns the binding this AuthNRequest will be sent and encoded with. If * {@link Saml2MessageBinding#REDIRECT} is used, the DEFLATE encoding will be @@ -104,9 +125,24 @@ public static class Builder> { String relayState; + String relyingPartyRegistrationId; + + /** + * @deprecated Use {@link #Builder(RelyingPartyRegistration)} instead + */ + @Deprecated protected Builder() { } + /** + * Creates a new Builder with relying party registration + * @param registration the registration of the relying party. + * @sine 5.8 + */ + protected Builder(RelyingPartyRegistration registration) { + this.relyingPartyRegistrationId = registration.getRegistrationId(); + } + /** * Casting the return as the generic subtype, when returning itself * @return this object diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java index 22c12e1ebf5..db478be3abc 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java @@ -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. @@ -17,6 +17,7 @@ package org.springframework.security.saml2.provider.service.authentication; import java.io.Serializable; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,14 +35,22 @@ public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPri private final Map> attributes; + private final List sessionIndexes; + private String registrationId; public DefaultSaml2AuthenticatedPrincipal(String name, Map> attributes) { + this(name, attributes, Collections.emptyList()); + } + + public DefaultSaml2AuthenticatedPrincipal(String name, Map> attributes, + List sessionIndexes) { Assert.notNull(name, "name cannot be null"); Assert.notNull(attributes, "attributes cannot be null"); + Assert.notNull(sessionIndexes, "sessionIndexes cannot be null"); this.name = name; this.attributes = attributes; - this.registrationId = null; + this.sessionIndexes = sessionIndexes; } @Override @@ -54,6 +63,11 @@ public Map> getAttributes() { return this.attributes; } + @Override + public List getSessionIndexes() { + return this.sessionIndexes; + } + @Override public String getRelyingPartyRegistrationId() { return this.registrationId; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java index c40015f94f8..205c5f8941c 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java @@ -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. @@ -76,4 +76,8 @@ default String getRelyingPartyRegistrationId() { return null; } + default List getSessionIndexes() { + return Collections.emptyList(); + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java index a8bf6aeb51f..18d6942fe16 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java @@ -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. @@ -28,7 +28,11 @@ * @since 5.3 * @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext) * @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext) + * @deprecated Use + * {@link org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver} + * instead */ +@Deprecated public class Saml2AuthenticationRequestContext { private final RelyingPartyRegistration relyingPartyRegistration; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java index db2b13585bd..7ae6c3dc703 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java @@ -29,7 +29,11 @@ * Page 50, Line 2147 * * @since 5.2 + * @deprecated As of 5.7.0, use + * {@link org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver} + * instead */ +@Deprecated public interface Saml2AuthenticationRequestFactory { /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java index 5fc84dd078a..a41f7bcb06a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java @@ -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. @@ -16,6 +16,7 @@ package org.springframework.security.saml2.provider.service.authentication; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; /** @@ -29,8 +30,9 @@ */ public class Saml2PostAuthenticationRequest extends AbstractSaml2AuthenticationRequest { - Saml2PostAuthenticationRequest(String samlRequest, String relayState, String authenticationRequestUri) { - super(samlRequest, relayState, authenticationRequestUri); + Saml2PostAuthenticationRequest(String samlRequest, String relayState, String authenticationRequestUri, + String relyingPartyRegistrationId) { + super(samlRequest, relayState, authenticationRequestUri, relyingPartyRegistrationId); } /** @@ -51,7 +53,19 @@ public Saml2MessageBinding getBinding() { * @return a modifiable builder object */ public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) { - return new Builder().authenticationRequestUri(context.getDestination()).relayState(context.getRelayState()); + return new Builder(context.getRelyingPartyRegistration()).authenticationRequestUri(context.getDestination()) + .relayState(context.getRelayState()); + } + + /** + * Constructs a {@link Builder} from a {@link RelyingPartyRegistration} object. + * @param registration a relying party registration + * @return a modifiable builder object + * @since 5.7 + */ + public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) { + String location = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation(); + return new Builder(registration).authenticationRequestUri(location); } /** @@ -59,7 +73,8 @@ public static Builder withAuthenticationRequestContext(Saml2AuthenticationReques */ public static final class Builder extends AbstractSaml2AuthenticationRequest.Builder { - private Builder() { + private Builder(RelyingPartyRegistration registration) { + super(registration); } /** @@ -67,7 +82,8 @@ private Builder() { * @return an immutable {@link Saml2PostAuthenticationRequest} object. */ public Saml2PostAuthenticationRequest build() { - return new Saml2PostAuthenticationRequest(this.samlRequest, this.relayState, this.authenticationRequestUri); + return new Saml2PostAuthenticationRequest(this.samlRequest, this.relayState, this.authenticationRequestUri, + this.relyingPartyRegistrationId); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java index 80fec1d392b..d58850fcc22 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java @@ -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. @@ -16,6 +16,7 @@ package org.springframework.security.saml2.provider.service.authentication; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; /** @@ -34,8 +35,8 @@ public final class Saml2RedirectAuthenticationRequest extends AbstractSaml2Authe private final String signature; private Saml2RedirectAuthenticationRequest(String samlRequest, String sigAlg, String signature, String relayState, - String authenticationRequestUri) { - super(samlRequest, relayState, authenticationRequestUri); + String authenticationRequestUri, String relyingPartyRegistrationId) { + super(samlRequest, relayState, authenticationRequestUri, relyingPartyRegistrationId); this.sigAlg = sigAlg; this.signature = signature; } @@ -74,7 +75,20 @@ public Saml2MessageBinding getBinding() { * @return a modifiable builder object */ public static Builder withAuthenticationRequestContext(Saml2AuthenticationRequestContext context) { - return new Builder().authenticationRequestUri(context.getDestination()).relayState(context.getRelayState()); + return new Builder(context.getRelyingPartyRegistration()).authenticationRequestUri(context.getDestination()) + .relayState(context.getRelayState()); + } + + /** + * Constructs a {@link Saml2PostAuthenticationRequest.Builder} from a + * {@link RelyingPartyRegistration} object. + * @param registration a relying party registration + * @return a modifiable builder object + * @since 5.7 + */ + public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) { + String location = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation(); + return new Builder(registration).authenticationRequestUri(location); } /** @@ -86,7 +100,8 @@ public static final class Builder extends AbstractSaml2AuthenticationRequest.Bui private String signature; - private Builder() { + private Builder(RelyingPartyRegistration registration) { + super(registration); } /** @@ -115,7 +130,7 @@ public Builder signature(String signature) { */ public Saml2RedirectAuthenticationRequest build() { return new Saml2RedirectAuthenticationRequest(this.samlRequest, this.sigAlg, this.signature, - this.relayState, this.authenticationRequestUri); + this.relayState, this.authenticationRequestUri, this.relyingPartyRegistrationId); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java index f8f1066a795..1d1012f702f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java @@ -19,13 +19,12 @@ 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 @@ */ 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.getEncoder().encodeToString(b); } static byte[] samlDecode(String s) { - return BASE64.decode(s); + return Base64.getMimeDecoder().decode(s); } static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java new file mode 100644 index 00000000000..5ff94a701b5 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication.logout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.saml2.core.EncryptedID; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; +import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +/** + * Utility methods for decrypting EncryptedID from SAML logout request with OpenSAML + * + * For internal use only. + * + * this is mainly a adapted copy of OpenSamlDecryptionUtils + * + * @author Robert Stoiber + */ +final class LogoutRequestEncryptedIdUtils { + + private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver())); + + static SAMLObject decryptEncryptedId(EncryptedID encryptedId, RelyingPartyRegistration registration) { + Decrypter decrypter = decrypter(registration); + try { + return decrypter.decrypt(encryptedId); + + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private static Decrypter decrypter(RelyingPartyRegistration registration) { + Collection credentials = new ArrayList<>(); + for (Saml2X509Credential key : registration.getDecryptionX509Credentials()) { + Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + credentials.add(cred); + } + KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials); + Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + + private LogoutRequestEncryptedIdUtils() { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java index 69df68246a2..5345aa88751 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -25,6 +25,8 @@ import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.LogoutRequest; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller; @@ -118,7 +120,7 @@ private Consumer> validateRequest(LogoutRequest request, return (errors) -> { validateIssuer(request, registration).accept(errors); validateDestination(request, registration).accept(errors); - validateName(request, authentication).accept(errors); + validateSubject(request, registration, authentication).accept(errors); }; } @@ -153,23 +155,49 @@ private Consumer> validateDestination(LogoutRequest reque }; } - private Consumer> validateName(LogoutRequest request, Authentication authentication) { + private Consumer> validateSubject(LogoutRequest request, + RelyingPartyRegistration registration, Authentication authentication) { return (errors) -> { if (authentication == null) { return; } - NameID nameId = request.getNameID(); + NameID nameId = getNameId(request, registration); if (nameId == null) { errors.add( new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest")); return; } - String name = nameId.getValue(); - if (!name.equals(authentication.getName())) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, - "Failed to match subject in LogoutRequest with currently logged in user")); - } + + validateNameId(nameId, authentication, errors); }; } + private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) { + NameID nameId = request.getNameID(); + if (nameId != null) { + return nameId; + } + EncryptedID encryptedId = request.getEncryptedID(); + if (encryptedId == null) { + return null; + } + return decryptNameId(encryptedId, registration); + } + + private void validateNameId(NameID nameId, Authentication authentication, Collection errors) { + String name = nameId.getValue(); + if (!name.equals(authentication.getName())) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, + "Failed to match subject in LogoutRequest with currently logged in user")); + } + } + + private NameID decryptNameId(EncryptedID encryptedId, RelyingPartyRegistration registration) { + final SAMLObject decryptedId = LogoutRequestEncryptedIdUtils.decryptEncryptedId(encryptedId, registration); + if (decryptedId instanceof NameID) { + return ((NameID) decryptedId); + } + return null; + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java index 975f7a7525e..24ea46312a1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -50,7 +50,7 @@ import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.web.util.UriUtils; +import org.springframework.web.util.UriComponentsBuilder; /** * Utility methods for verifying SAML component signatures with OpenSAML @@ -191,8 +191,9 @@ private static class RedirectSignature { else { this.signature = null; } - this.content = content(request.getSamlRequest(), Saml2ParameterNames.SAML_REQUEST, - request.getRelayState(), request.getParameter(Saml2ParameterNames.SIG_ALG)); + this.content = UriComponentsBuilder.newInstance().query(request.getParametersQuery()) + .replaceQueryParam(Saml2ParameterNames.SIGNATURE).build(true).toUriString().substring(1) + .getBytes(StandardCharsets.UTF_8); } RedirectSignature(Saml2LogoutResponse response) { @@ -203,22 +204,9 @@ private static class RedirectSignature { else { this.signature = null; } - this.content = content(response.getSamlResponse(), Saml2ParameterNames.SAML_RESPONSE, - response.getRelayState(), response.getParameter(Saml2ParameterNames.SIG_ALG)); - } - - static byte[] content(String samlObject, String objectParameterName, String relayState, String algorithm) { - if (relayState != null) { - return String.format("%s=%s&%s=%s&%s=%s", objectParameterName, - UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1), Saml2ParameterNames.RELAY_STATE, - UriUtils.encode(relayState, StandardCharsets.ISO_8859_1), Saml2ParameterNames.SIG_ALG, - UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1)).getBytes(StandardCharsets.UTF_8); - } - else { - return String.format("%s=%s&%s=%s", objectParameterName, - UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1), Saml2ParameterNames.SIG_ALG, - UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1)).getBytes(StandardCharsets.UTF_8); - } + this.content = UriComponentsBuilder.newInstance().query(response.getParametersQuery()) + .replaceQueryParam(Saml2ParameterNames.SIGNATURE).build(true).toUriString().substring(1) + .getBytes(StandardCharsets.UTF_8); } byte[] getContent() { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java index 3d35db39578..836de13a3b8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java @@ -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. @@ -17,15 +17,19 @@ package org.springframework.security.saml2.provider.service.authentication.logout; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; /** * A class that represents a signed and serialized SAML 2.0 Logout Request @@ -35,6 +39,17 @@ */ public final class Saml2LogoutRequest implements Serializable { + private static final Function, String> DEFAULT_ENCODER = (params) -> { + if (params.isEmpty()) { + return null; + } + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : params.entrySet()) { + builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + return builder.build(true).toString().substring(1); + }; + private final String location; private final Saml2MessageBinding binding; @@ -45,13 +60,21 @@ public final class Saml2LogoutRequest implements Serializable { private final String relyingPartyRegistrationId; + private Function, String> encoder; + private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map parameters, String id, String relyingPartyRegistrationId) { + this(location, binding, parameters, id, relyingPartyRegistrationId, DEFAULT_ENCODER); + } + + private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map parameters, String id, + String relyingPartyRegistrationId, Function, String> encoder) { this.location = location; this.binding = binding; - this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); + this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters)); this.id = id; this.relyingPartyRegistrationId = relyingPartyRegistrationId; + this.encoder = encoder; } /** @@ -119,6 +142,16 @@ public Map getParameters() { return this.parameters; } + /** + * Get an encoded query string of all parameters. Resulting query does not contain a + * leading question mark. + * @return an encoded string of all parameters + * @since 5.8 + */ + public String getParametersQuery() { + return this.encoder.apply(this.parameters); + } + /** * The identifier for the {@link RelyingPartyRegistration} associated with this Logout * Request @@ -149,7 +182,9 @@ public static final class Builder { private Saml2MessageBinding binding; - private Map parameters = new HashMap<>(); + private Map parameters = new LinkedHashMap<>(); + + private Function, String> encoder = DEFAULT_ENCODER; private String id; @@ -235,13 +270,28 @@ public Builder parameters(Consumer> parametersConsumer) { return this; } + /** + * Use this strategy for converting parameters into an encoded query string. The + * resulting query does not contain a leading question mark. + * + * In the event that you already have an encoded version that you want to use, you + * can call this by doing {@code parameterEncoder((params) -> encodedValue)}. + * @param encoder the strategy to use + * @return the {@link Builder} for further configurations + * @since 5.8 + */ + public Builder parametersQuery(Function, String> encoder) { + this.encoder = encoder; + return this; + } + /** * Build the {@link Saml2LogoutRequest} * @return a constructed {@link Saml2LogoutRequest} */ public Saml2LogoutRequest build() { return new Saml2LogoutRequest(this.location, this.binding, this.parameters, this.id, - this.registration.getRegistrationId()); + this.registration.getRegistrationId(), this.encoder); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java index 43d64cf052e..a555b784a22 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,19 @@ package org.springframework.security.saml2.provider.service.authentication.logout; +import java.nio.charset.StandardCharsets; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; /** * A class that represents a signed and serialized SAML 2.0 Logout Response @@ -34,16 +38,31 @@ */ public final class Saml2LogoutResponse { + private static final Function, String> DEFAULT_ENCODER = (params) -> { + if (params.isEmpty()) { + return null; + } + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : params.entrySet()) { + builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + return builder.build(true).toString().substring(1); + }; + private final String location; private final Saml2MessageBinding binding; private final Map parameters; - private Saml2LogoutResponse(String location, Saml2MessageBinding binding, Map parameters) { + private final Function, String> encoder; + + private Saml2LogoutResponse(String location, Saml2MessageBinding binding, Map parameters, + Function, String> encoder) { this.location = location; this.binding = binding; - this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); + this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters)); + this.encoder = encoder; } /** @@ -103,6 +122,16 @@ public Map getParameters() { return this.parameters; } + /** + * Get an encoded query string of all parameters. Resulting query does not contain a + * leading question mark. + * @return an encoded string of all parameters + * @since 5.8 + */ + public String getParametersQuery() { + return this.encoder.apply(this.parameters); + } + /** * Create a {@link Builder} instance from this {@link RelyingPartyRegistration} * @@ -122,7 +151,9 @@ public static final class Builder { private Saml2MessageBinding binding; - private Map parameters = new HashMap<>(); + private Map parameters = new LinkedHashMap<>(); + + private Function, String> encoder = DEFAULT_ENCODER; private Builder(RelyingPartyRegistration registration) { this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation(); @@ -195,12 +226,27 @@ public Builder parameters(Consumer> parametersConsumer) { return this; } + /** + * Use this strategy for converting parameters into an encoded query string. The + * resulting query does not contain a leading question mark. + * + * In the event that you already have an encoded version that you want to use, you + * can call this by doing {@code parameterEncoder((params) -> encodedValue)}. + * @param encoder the strategy to use + * @return the {@link Saml2LogoutRequest.Builder} for further configurations + * @since 5.8 + */ + public Builder parametersQuery(Function, String> encoder) { + this.encoder = encoder; + return this; + } + /** * Build the {@link Saml2LogoutResponse} * @return a constructed {@link Saml2LogoutResponse} */ public Saml2LogoutResponse build() { - return new Saml2LogoutResponse(this.location, this.binding, this.parameters); + return new Saml2LogoutResponse(this.location, this.binding, this.parameters, this.encoder); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java index 0190a85dfb9..3f1c9e0026d 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java @@ -44,7 +44,7 @@ static String samlEncode(byte[] b) { } static byte[] samlDecode(String s) { - return Base64.getDecoder().decode(s); + return Base64.getMimeDecoder().decode(s); } static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java index 1f0d5c19af1..4e0ad7f6a2b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java @@ -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. @@ -21,6 +21,7 @@ import java.util.Base64; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import javax.xml.namespace.QName; @@ -31,6 +32,7 @@ import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.NameIDFormat; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml.saml2.metadata.SingleLogoutService; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; @@ -44,6 +46,7 @@ import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.util.Assert; /** @@ -62,6 +65,9 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { private final EntityDescriptorMarshaller entityDescriptorMarshaller; + private Consumer entityDescriptorCustomizer = (parameters) -> { + }; + public OpenSamlMetadataResolver() { this.entityDescriptorMarshaller = (EntityDescriptorMarshaller) XMLObjectProviderRegistrySupport .getMarshallerFactory().getMarshaller(EntityDescriptor.DEFAULT_ELEMENT_NAME); @@ -70,23 +76,42 @@ public OpenSamlMetadataResolver() { @Override public String resolve(RelyingPartyRegistration relyingPartyRegistration) { - EntityDescriptor entityDescriptor = build(EntityDescriptor.ELEMENT_QNAME); + EntityDescriptor entityDescriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME); entityDescriptor.setEntityID(relyingPartyRegistration.getEntityId()); SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(relyingPartyRegistration); entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor); + this.entityDescriptorCustomizer + .accept(new EntityDescriptorParameters(entityDescriptor, relyingPartyRegistration)); return serialize(entityDescriptor); } + /** + * Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor} + * @param entityDescriptorCustomizer a consumer that accepts an + * {@link EntityDescriptorParameters} + * @since 5.7 + */ + public void setEntityDescriptorCustomizer(Consumer entityDescriptorCustomizer) { + Assert.notNull(entityDescriptorCustomizer, "entityDescriptorCustomizer cannot be null"); + this.entityDescriptorCustomizer = entityDescriptorCustomizer; + } + private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) { SPSSODescriptor spSsoDescriptor = build(SPSSODescriptor.DEFAULT_ELEMENT_NAME); spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); - spSsoDescriptor.setWantAssertionsSigned(true); spSsoDescriptor.getKeyDescriptors() .addAll(buildKeys(registration.getSigningX509Credentials(), UsageType.SIGNING)); spSsoDescriptor.getKeyDescriptors() .addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION)); spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration)); - spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration)); + if (registration.getSingleLogoutServiceLocation() != null) { + for (Saml2MessageBinding binding : registration.getSingleLogoutServiceBindings()) { + spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration, binding)); + } + } + if (registration.getNameIdFormat() != null) { + spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration)); + } return spSsoDescriptor; } @@ -125,14 +150,21 @@ private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegis return assertionConsumerService; } - private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration) { + private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration, + Saml2MessageBinding binding) { SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME); singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation()); singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation()); - singleLogoutService.setBinding(registration.getSingleLogoutServiceBinding().getUrn()); + singleLogoutService.setBinding(binding.getUrn()); return singleLogoutService; } + private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) { + NameIDFormat nameIdFormat = build(NameIDFormat.DEFAULT_ELEMENT_NAME); + nameIdFormat.setFormat(registration.getNameIdFormat()); + return nameIdFormat; + } + @SuppressWarnings("unchecked") private T build(QName elementName) { XMLObjectBuilder builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName); @@ -152,4 +184,31 @@ private String serialize(EntityDescriptor entityDescriptor) { } } + /** + * A tuple containing an OpenSAML {@link EntityDescriptor} and its associated + * {@link RelyingPartyRegistration} + * + * @since 5.7 + */ + public static final class EntityDescriptorParameters { + + private final EntityDescriptor entityDescriptor; + + private final RelyingPartyRegistration registration; + + public EntityDescriptorParameters(EntityDescriptor entityDescriptor, RelyingPartyRegistration registration) { + this.entityDescriptor = entityDescriptor; + this.registration = registration; + } + + public EntityDescriptor getEntityDescriptor() { + return this.entityDescriptor; + } + + public RelyingPartyRegistration getRelyingPartyRegistration() { + return this.registration; + } + + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java new file mode 100644 index 00000000000..5a224ac8688 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import org.opensaml.saml.saml2.metadata.EntityDescriptor; + +/** + * A {@link RelyingPartyRegistration.AssertingPartyDetails} that contains + * OpenSAML-specific members + * + * @author Josh Cummings + * @since 5.7 + */ +public final class OpenSamlAssertingPartyDetails extends RelyingPartyRegistration.AssertingPartyDetails { + + private final EntityDescriptor descriptor; + + OpenSamlAssertingPartyDetails(RelyingPartyRegistration.AssertingPartyDetails details, EntityDescriptor descriptor) { + super(details.getEntityId(), details.getWantAuthnRequestsSigned(), details.getSigningAlgorithms(), + details.getVerificationX509Credentials(), details.getEncryptionX509Credentials(), + details.getSingleSignOnServiceLocation(), details.getSingleSignOnServiceBinding(), + details.getSingleLogoutServiceLocation(), details.getSingleLogoutServiceResponseLocation(), + details.getSingleLogoutServiceBinding()); + this.descriptor = descriptor; + } + + /** + * Get the {@link EntityDescriptor} that underlies this + * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails} + * @return the {@link EntityDescriptor} + */ + public EntityDescriptor getEntityDescriptor() { + return this.descriptor; + } + + /** + * Use this {@link EntityDescriptor} to begin building an + * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails} + * @param entity the {@link EntityDescriptor} to use + * @return the + * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlAssertingPartyDetails.Builder} + * for further configurations + */ + public static OpenSamlAssertingPartyDetails.Builder withEntityDescriptor(EntityDescriptor entity) { + return new OpenSamlAssertingPartyDetails.Builder(entity); + } + + /** + * An OpenSAML version of + * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails.Builder} + * that contains the underlying {@link EntityDescriptor} + */ + public static final class Builder extends RelyingPartyRegistration.AssertingPartyDetails.Builder { + + private final EntityDescriptor descriptor; + + private Builder(EntityDescriptor descriptor) { + this.descriptor = descriptor; + } + + /** + * Build an + * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlAssertingPartyDetails} + * @return + */ + @Override + public OpenSamlAssertingPartyDetails build() { + return new OpenSamlAssertingPartyDetails(super.build(), this.descriptor); + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java similarity index 76% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java rename to saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java index 1b0eb0e35a9..dbe5cff5822 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java @@ -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. @@ -20,6 +20,8 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import net.shibboleth.utilities.java.support.xml.ParserPool; @@ -45,7 +47,7 @@ import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2X509Credential; -class OpenSamlAssertingPartyMetadataConverter { +class OpenSamlMetadataAssertingPartyDetailsConverter { static { OpenSamlInitializationService.initialize(); @@ -56,15 +58,31 @@ class OpenSamlAssertingPartyMetadataConverter { private final ParserPool parserPool; /** - * Creates a {@link OpenSamlAssertingPartyMetadataConverter} + * Creates a {@link OpenSamlMetadataAssertingPartyDetailsConverter} */ - OpenSamlAssertingPartyMetadataConverter() { + OpenSamlMetadataAssertingPartyDetailsConverter() { this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); this.parserPool = this.registry.getParserPool(); } - RelyingPartyRegistration.Builder convert(InputStream inputStream) { - EntityDescriptor descriptor = entityDescriptor(inputStream); + Collection convert(InputStream inputStream) { + List builders = new ArrayList<>(); + XMLObject xmlObject = xmlObject(inputStream); + if (xmlObject instanceof EntitiesDescriptor) { + EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject; + for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) { + builders.add(convert(descriptor)); + } + return builders; + } + if (xmlObject instanceof EntityDescriptor) { + EntityDescriptor descriptor = (EntityDescriptor) xmlObject; + return Arrays.asList(convert(descriptor)); + } + throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass()); + } + + RelyingPartyRegistration.AssertingPartyDetails.Builder convert(EntityDescriptor descriptor) { IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); if (idpssoDescriptor == null) { throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); @@ -96,15 +114,14 @@ RelyingPartyRegistration.Builder convert(InputStream inputStream) { throw new Saml2Exception( "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); } - RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(descriptor.getEntityID()) - .assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID()) - .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) - .verificationX509Credentials((c) -> c.addAll(verification)) - .encryptionX509Credentials((c) -> c.addAll(encryption))); + RelyingPartyRegistration.AssertingPartyDetails.Builder party = OpenSamlAssertingPartyDetails + .withEntityDescriptor(descriptor).entityId(descriptor.getEntityID()) + .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) + .verificationX509Credentials((c) -> c.addAll(verification)) + .encryptionX509Credentials((c) -> c.addAll(encryption)); List signingMethods = signingMethods(idpssoDescriptor); for (SigningMethod method : signingMethods) { - builder.assertingPartyDetails( - (party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm()))); + party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())); } if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) { throw new Saml2Exception( @@ -121,9 +138,7 @@ else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.ge else { continue; } - builder.assertingPartyDetails( - (party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) - .singleSignOnServiceBinding(binding)); + party.singleSignOnServiceLocation(singleSignOnService.getLocation()).singleSignOnServiceBinding(binding); break; } for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) { @@ -139,12 +154,11 @@ else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.ge } String responseLocation = (singleLogoutService.getResponseLocation() == null) ? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation(); - builder.assertingPartyDetails( - (party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation()) - .singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding)); + party.singleLogoutServiceLocation(singleLogoutService.getLocation()) + .singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding); break; } - return builder; + return party; } private List certificates(KeyDescriptor keyDescriptor) { @@ -167,7 +181,7 @@ private List signingMethods(IDPSSODescriptor idpssoDescriptor) { return signingMethods(extensions); } - private EntityDescriptor entityDescriptor(InputStream inputStream) { + private XMLObject xmlObject(InputStream inputStream) { Document document = document(inputStream); Element element = document.getDocumentElement(); Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); @@ -175,18 +189,11 @@ private EntityDescriptor entityDescriptor(InputStream inputStream) { throw new Saml2Exception("Unsupported element of type " + element.getTagName()); } try { - XMLObject object = unmarshaller.unmarshall(element); - if (object instanceof EntitiesDescriptor) { - return ((EntitiesDescriptor) object).getEntityDescriptors().get(0); - } - if (object instanceof EntityDescriptor) { - return (EntityDescriptor) object; - } + return unmarshaller.unmarshall(element); } catch (Exception ex) { throw new Saml2Exception(ex); } - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); } private Document document(InputStream inputStream) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java index 6e5284a941d..8f29e833587 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java @@ -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. @@ -62,13 +62,13 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter OpenSamlInitializationService.initialize(); } - private final OpenSamlAssertingPartyMetadataConverter converter; + private final OpenSamlMetadataAssertingPartyDetailsConverter converter; /** * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter} */ public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() { - this.converter = new OpenSamlAssertingPartyMetadataConverter(); + this.converter = new OpenSamlMetadataAssertingPartyDetailsConverter(); } @Override @@ -89,7 +89,8 @@ public List getSupportedMediaTypes() { @Override public RelyingPartyRegistration.Builder read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - return this.converter.convert(inputMessage.getBody()); + return RelyingPartyRegistration + .withAssertingPartyDetails(this.converter.convert(inputMessage.getBody()).iterator().next().build()); } @Override diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index d07a3664f8a..64dcde6a7ed 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -21,7 +21,7 @@ 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; @@ -32,6 +32,7 @@ import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Represents a configured relying party (aka Service Provider) and asserting party (aka @@ -85,7 +86,9 @@ public final class RelyingPartyRegistration { private final String singleLogoutServiceResponseLocation; - private final Saml2MessageBinding singleLogoutServiceBinding; + private final Collection singleLogoutServiceBindings; + + private final String nameIdFormat; private final ProviderDetails providerDetails; @@ -97,8 +100,8 @@ public final class RelyingPartyRegistration { private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation, - String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding, - ProviderDetails providerDetails, + String singleLogoutServiceResponseLocation, Collection singleLogoutServiceBindings, + ProviderDetails providerDetails, String nameIdFormat, Collection credentials, Collection decryptionX509Credentials, Collection signingX509Credentials) { @@ -106,8 +109,12 @@ private RelyingPartyRegistration(String registrationId, String entityId, String 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 || !CollectionUtils.isEmpty(singleLogoutServiceBindings), + "singleLogoutServiceBindings cannot be null or empty when singleLogoutServiceLocation is set"); Assert.notNull(providerDetails, "providerDetails cannot be null"); - Assert.notEmpty(credentials, "credentials cannot be empty"); + Assert.isTrue( + !credentials.isEmpty() || (decryptionX509Credentials.isEmpty() && signingX509Credentials.isEmpty()), + "credentials cannot be empty"); for (org.springframework.security.saml2.credentials.Saml2X509Credential c : credentials) { Assert.notNull(c, "credentials cannot contain null elements"); } @@ -128,7 +135,8 @@ private RelyingPartyRegistration(String registrationId, String entityId, String this.assertionConsumerServiceBinding = assertionConsumerServiceBinding; this.singleLogoutServiceLocation = singleLogoutServiceLocation; this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; - this.singleLogoutServiceBinding = singleLogoutServiceBinding; + this.singleLogoutServiceBindings = Collections.unmodifiableList(new LinkedList<>(singleLogoutServiceBindings)); + this.nameIdFormat = nameIdFormat; this.providerDetails = providerDetails; this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials)); this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials)); @@ -201,7 +209,22 @@ public Saml2MessageBinding getAssertionConsumerServiceBinding() { * @since 5.6 */ public Saml2MessageBinding getSingleLogoutServiceBinding() { - return this.singleLogoutServiceBinding; + Assert.state(this.singleLogoutServiceBindings.size() == 1, "Method does not support multiple bindings."); + return this.singleLogoutServiceBindings.iterator().next(); + } + + /** + * Get the SingleLogoutService + * Binding + *

      + * Equivalent to the value found in <SingleLogoutService Binding="..."/> in the + * relying party's <SPSSODescriptor>. + * @return the SingleLogoutService Binding + * @since 5.8 + */ + public Collection getSingleLogoutServiceBindings() { + return this.singleLogoutServiceBindings; } /** @@ -234,6 +257,15 @@ public String getSingleLogoutServiceResponseLocation() { return this.singleLogoutServiceResponseLocation; } + /** + * Get the NameID format. + * @return the NameID format + * @since 5.7 + */ + public String getNameIdFormat() { + return this.nameIdFormat; + } + /** * Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated * with this relying party @@ -408,6 +440,21 @@ public static Builder withRegistrationId(String registrationId) { return new Builder(registrationId); } + public static Builder withAssertingPartyDetails(AssertingPartyDetails assertingPartyDetails) { + Assert.notNull(assertingPartyDetails, "assertingPartyDetails cannot be null"); + return withRegistrationId(assertingPartyDetails.getEntityId()).assertingPartyDetails((party) -> party + .entityId(assertingPartyDetails.getEntityId()) + .wantAuthnRequestsSigned(assertingPartyDetails.getWantAuthnRequestsSigned()) + .signingAlgorithms((algorithms) -> algorithms.addAll(assertingPartyDetails.getSigningAlgorithms())) + .verificationX509Credentials((c) -> c.addAll(assertingPartyDetails.getVerificationX509Credentials())) + .encryptionX509Credentials((c) -> c.addAll(assertingPartyDetails.getEncryptionX509Credentials())) + .singleSignOnServiceLocation(assertingPartyDetails.getSingleSignOnServiceLocation()) + .singleSignOnServiceBinding(assertingPartyDetails.getSingleSignOnServiceBinding()) + .singleLogoutServiceLocation(assertingPartyDetails.getSingleLogoutServiceLocation()) + .singleLogoutServiceResponseLocation(assertingPartyDetails.getSingleLogoutServiceResponseLocation()) + .singleLogoutServiceBinding(assertingPartyDetails.getSingleLogoutServiceBinding())); + } + /** * Creates a {@code RelyingPartyRegistration} {@link Builder} based on an existing * object @@ -423,7 +470,8 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi .assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding()) .singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation()) .singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation()) - .singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding()) + .singleLogoutServiceBindings((c) -> c.addAll(registration.getSingleLogoutServiceBindings())) + .nameIdFormat(registration.getNameIdFormat()) .assertingPartyDetails((assertingParty) -> assertingParty .entityId(registration.getAssertingPartyDetails().getEntityId()) .wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) @@ -449,7 +497,7 @@ private static Saml2X509Credential fromDeprecated( org.springframework.security.saml2.credentials.Saml2X509Credential credential) { PrivateKey privateKey = credential.getPrivateKey(); X509Certificate certificate = credential.getCertificate(); - Set credentialTypes = new HashSet<>(); + Set credentialTypes = new LinkedHashSet<>(); if (credential.isSigningCredential()) { credentialTypes.add(Saml2X509Credential.Saml2X509CredentialType.SIGNING); } @@ -469,7 +517,7 @@ private static org.springframework.security.saml2.credentials.Saml2X509Credentia Saml2X509Credential credential) { PrivateKey privateKey = credential.getPrivateKey(); X509Certificate certificate = credential.getCertificate(); - Set credentialTypes = new HashSet<>(); + Set credentialTypes = new LinkedHashSet<>(); if (credential.isSigningCredential()) { credentialTypes.add( org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING); @@ -495,7 +543,7 @@ private static org.springframework.security.saml2.credentials.Saml2X509Credentia * * @since 5.4 */ - public static final class AssertingPartyDetails { + public static class AssertingPartyDetails { private final String entityId; @@ -517,7 +565,7 @@ public static final class AssertingPartyDetails { private final Saml2MessageBinding singleLogoutServiceBinding; - private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List signingAlgorithms, + AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List signingAlgorithms, Collection verificationX509Credentials, Collection encryptionX509Credentials, String singleSignOnServiceLocation, Saml2MessageBinding singleSignOnServiceBinding, String singleLogoutServiceLocation, @@ -686,7 +734,7 @@ public Saml2MessageBinding getSingleLogoutServiceBinding() { return this.singleLogoutServiceBinding; } - public static final class Builder { + public static class Builder { private String entityId; @@ -694,9 +742,9 @@ public static final class Builder { private List signingAlgorithms = new ArrayList<>(); - private Collection verificationX509Credentials = new HashSet<>(); + private Collection verificationX509Credentials = new LinkedHashSet<>(); - private Collection encryptionX509Credentials = new HashSet<>(); + private Collection encryptionX509Credentials = new LinkedHashSet<>(); private String singleSignOnServiceLocation; @@ -936,7 +984,7 @@ public Saml2MessageBinding getBinding() { @Deprecated public static final class Builder { - private final AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder(); + private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder(); /** * Set the asserting party's signingX509Credentials = new HashSet<>(); + private Collection signingX509Credentials = new LinkedHashSet<>(); - private Collection decryptionX509Credentials = new HashSet<>(); + private Collection 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; - private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST; + private Collection singleLogoutServiceBindings = new LinkedHashSet<>(); + + private String nameIdFormat = null; private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder(); - private Collection credentials = new HashSet<>(); + private Collection credentials = new LinkedHashSet<>(); private Builder(String registrationId) { this.registrationId = registrationId; @@ -1134,7 +1184,28 @@ public Builder assertionConsumerServiceBinding(Saml2MessageBinding assertionCons * @since 5.6 */ public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) { - this.singleLogoutServiceBinding = singleLogoutServiceBinding; + return this.singleLogoutServiceBindings((saml2MessageBindings) -> { + saml2MessageBindings.clear(); + saml2MessageBindings.add(singleLogoutServiceBinding); + }); + } + + /** + * Apply this {@link Consumer} to the {@link Collection} of + * {@link Saml2MessageBinding}s for the purposes of modifying the SingleLogoutService + * Binding {@link Collection}. + * + *

      + * Equivalent to the value found in <SingleLogoutService Binding="..."/> in + * the relying party's <SPSSODescriptor>. + * @param bindingsConsumer - the {@link Consumer} for modifying the + * {@link Collection} + * @return the {@link Builder} for further configuration + * @since 5.8 + */ + public Builder singleLogoutServiceBindings(Consumer> bindingsConsumer) { + bindingsConsumer.accept(this.singleLogoutServiceBindings); return this; } @@ -1173,6 +1244,17 @@ public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceRes return this; } + /** + * Set the NameID format + * @param nameIdFormat + * @return the {@link Builder} for further configuration + * @since 5.7 + */ + public Builder nameIdFormat(String nameIdFormat) { + this.nameIdFormat = nameIdFormat; + return this; + } + /** * Apply this {@link Consumer} to further configure the Asserting Party details * @param assertingPartyDetails The {@link Consumer} to apply @@ -1318,10 +1400,15 @@ public RelyingPartyRegistration build() { if (this.singleLogoutServiceResponseLocation == null) { this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation; } + + if (this.singleLogoutServiceBindings.isEmpty()) { + this.singleLogoutServiceBindings.add(Saml2MessageBinding.POST); + } + return new RelyingPartyRegistration(this.registrationId, this.entityId, this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation, - this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials, + this.singleLogoutServiceBindings, this.providerDetails.build(), this.nameIdFormat, this.credentials, this.decryptionX509Credentials, this.signingX509Credentials); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java index 2ed5246f64e..9b116331cb4 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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,10 +18,13 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails; /** * A utility class for constructing instances of {@link RelyingPartyRegistration} @@ -33,7 +36,7 @@ */ public final class RelyingPartyRegistrations { - private static final OpenSamlAssertingPartyMetadataConverter assertingPartyMetadataConverter = new OpenSamlAssertingPartyMetadataConverter(); + private static final OpenSamlMetadataAssertingPartyDetailsConverter assertingPartyMetadataConverter = new OpenSamlMetadataAssertingPartyDetailsConverter(); private static final ResourceLoader resourceLoader = new DefaultResourceLoader(); @@ -122,7 +125,101 @@ public static RelyingPartyRegistration.Builder fromMetadataLocation(String metad * @since 5.6 */ public static RelyingPartyRegistration.Builder fromMetadata(InputStream source) { - return assertingPartyMetadataConverter.convert(source); + return collectionFromMetadata(source).iterator().next(); + } + + /** + * Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off + * of the given SAML 2.0 Asserting Party (IDP) metadata location. + * + * Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some + * valid endpoints might include: + * + *

      +	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
      +	 *   metadataLocation = "file:asserting-party-metadata.xml";
      +	 *   metadataLocation = "https://ap.example.org/metadata";
      +	 * 
      + * + * Note that by default the registrationId is set to be the given metadata location, + * but this will most often not be sufficient. To complete the configuration, most + * applications will also need to provide a registrationId, like so: + * + *
      +	 *	Iterable<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
      +	 * 			.collectionFromMetadataLocation(location).iterator();
      +	 * 	RelyingPartyRegistration one = registrations.next().registrationId("one").build();
      +	 * 	RelyingPartyRegistration two = registrations.next().registrationId("two").build();
      +	 * 	return new InMemoryRelyingPartyRegistrationRepository(one, two);
      +	 * 
      + * + * Also note that an {@code IDPSSODescriptor} typically only contains information + * about the asserting party. Thus, you will need to remember to still populate + * anything about the relying party, like any private keys the relying party will use + * for signing AuthnRequests. + * @param location The classpath- or file-based locations or HTTP endpoints of the + * asserting party metadata file + * @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for + * further configuration + * @since 5.7 + */ + public static Collection collectionFromMetadataLocation(String location) { + try (InputStream source = resourceLoader.getResource(location).getInputStream()) { + return collectionFromMetadata(source); + } + catch (IOException ex) { + if (ex.getCause() instanceof Saml2Exception) { + throw (Saml2Exception) ex.getCause(); + } + throw new Saml2Exception(ex); + } + } + + /** + * Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off + * of the given SAML 2.0 Asserting Party (IDP) metadata. + * + *

      + * This method is intended for scenarios when the metadata is looked up by a separate + * mechanism. One such example is when the metadata is stored in a database. + *

      + * + *

      + * The callers of this method are accountable for closing the + * {@code InputStream} source. + *

      + * + * Note that by default the registrationId is set to be the given metadata location, + * but this will most often not be sufficient. To complete the configuration, most + * applications will also need to provide a registrationId, like so: + * + *
      +	 *	String xml = fromDatabase();
      +	 *	try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
      +	 *		Iterator<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
      +	 * 				.collectionFromMetadata(source).iterator();
      +	 * 		RelyingPartyRegistration one = registrations.next().registrationId("one").build();
      +	 * 		RelyingPartyRegistration two = registrations.next().registrationId("two").build();
      +	 * 		return new InMemoryRelyingPartyRegistrationRepository(one, two);
      +	 * 	}
      +	 * 
      + * + * Also note that an {@code IDPSSODescriptor} typically only contains information + * about the asserting party. Thus, you will need to remember to still populate + * anything about the relying party, like any private keys the relying party will use + * for signing AuthnRequests. + * @param source the {@link InputStream} source containing the asserting party + * metadata + * @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for + * further configuration + * @since 5.7 + */ + public static Collection collectionFromMetadata(InputStream source) { + Collection builders = new ArrayList<>(); + for (AssertingPartyDetails.Builder builder : assertingPartyMetadataConverter.convert(source)) { + builders.add(RelyingPartyRegistration.withAssertingPartyDetails(builder.build())); + } + return builders; } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java index 0d5a018c361..afa6d371ba3 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java @@ -24,7 +24,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; @@ -37,7 +36,6 @@ import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * @since 5.2 @@ -95,8 +93,7 @@ public Saml2WebSsoAuthenticationFilter(AuthenticationConverter authenticationCon @Override protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { - return (super.requiresAuthentication(request, response) - && StringUtils.hasText(request.getParameter(Saml2ParameterNames.SAML_RESPONSE))); + return super.requiresAuthentication(request, response); } @Override diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java index 8eea145fe37..e6ee29a71bb 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java @@ -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. @@ -42,6 +42,7 @@ import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; @@ -78,11 +79,7 @@ */ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter { - private final Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver; - - private Saml2AuthenticationRequestFactory authenticationRequestFactory; - - private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}"); + private final Saml2AuthenticationRequestResolver authenticationRequestResolver; private Saml2AuthenticationRequestRepository authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository(); @@ -129,11 +126,20 @@ private static Saml2AuthenticationRequestFactory requestFactory() { public Saml2WebSsoAuthenticationRequestFilter( Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver, Saml2AuthenticationRequestFactory authenticationRequestFactory) { + this(new FactorySaml2AuthenticationRequestResolver(authenticationRequestContextResolver, + authenticationRequestFactory)); + } - Assert.notNull(authenticationRequestContextResolver, "authenticationRequestContextResolver cannot be null"); - Assert.notNull(authenticationRequestFactory, "authenticationRequestFactory cannot be null"); - this.authenticationRequestContextResolver = authenticationRequestContextResolver; - this.authenticationRequestFactory = authenticationRequestFactory; + /** + * Construct a {@link Saml2WebSsoAuthenticationRequestFilter} with the strategy for + * resolving the {@code AuthnRequest} + * @param authenticationRequestResolver the strategy for resolving the + * {@code AuthnRequest} + * @since 5.7 + */ + public Saml2WebSsoAuthenticationRequestFilter(Saml2AuthenticationRequestResolver authenticationRequestResolver) { + Assert.notNull(authenticationRequestResolver, "authenticationRequestResolver cannot be null"); + this.authenticationRequestResolver = authenticationRequestResolver; } /** @@ -146,16 +152,23 @@ public Saml2WebSsoAuthenticationRequestFilter( @Deprecated public void setAuthenticationRequestFactory(Saml2AuthenticationRequestFactory authenticationRequestFactory) { Assert.notNull(authenticationRequestFactory, "authenticationRequestFactory cannot be null"); - this.authenticationRequestFactory = authenticationRequestFactory; + Assert.isInstanceOf(FactorySaml2AuthenticationRequestResolver.class, this.authenticationRequestResolver, + "You cannot supply both a Saml2AuthenticationRequestResolver and a Saml2AuthenticationRequestFactory"); + ((FactorySaml2AuthenticationRequestResolver) this.authenticationRequestResolver).authenticationRequestFactory = authenticationRequestFactory; } /** * Use the given {@link RequestMatcher} that activates this filter for a given request * @param redirectMatcher the {@link RequestMatcher} to use + * @deprecated Configure the request matcher in an implementation of + * {@link Saml2AuthenticationRequestResolver} instead */ + @Deprecated public void setRedirectMatcher(RequestMatcher redirectMatcher) { Assert.notNull(redirectMatcher, "redirectMatcher cannot be null"); - this.redirectMatcher = redirectMatcher; + Assert.isInstanceOf(FactorySaml2AuthenticationRequestResolver.class, this.authenticationRequestResolver, + "You cannot supply a Saml2AuthenticationRequestResolver and a redirect matcher"); + ((FactorySaml2AuthenticationRequestResolver) this.authenticationRequestResolver).redirectMatcher = redirectMatcher; } /** @@ -174,30 +187,21 @@ public void setAuthenticationRequestRepository( @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - MatchResult matcher = this.redirectMatcher.matcher(request); - if (!matcher.isMatch()) { + AbstractSaml2AuthenticationRequest authenticationRequest = this.authenticationRequestResolver.resolve(request); + if (authenticationRequest == null) { filterChain.doFilter(request, response); return; } - - Saml2AuthenticationRequestContext context = this.authenticationRequestContextResolver.resolve(request); - if (context == null) { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - RelyingPartyRegistration relyingParty = context.getRelyingPartyRegistration(); - if (relyingParty.getAssertingPartyDetails().getSingleSignOnServiceBinding() == Saml2MessageBinding.REDIRECT) { - sendRedirect(request, response, context); + if (authenticationRequest instanceof Saml2RedirectAuthenticationRequest) { + sendRedirect(request, response, (Saml2RedirectAuthenticationRequest) authenticationRequest); } else { - sendPost(request, response, context); + sendPost(request, response, (Saml2PostAuthenticationRequest) authenticationRequest); } } private void sendRedirect(HttpServletRequest request, HttpServletResponse response, - Saml2AuthenticationRequestContext context) throws IOException { - Saml2RedirectAuthenticationRequest authenticationRequest = this.authenticationRequestFactory - .createRedirectAuthenticationRequest(context); + Saml2RedirectAuthenticationRequest authenticationRequest) throws IOException { this.authenticationRequestRepository.saveAuthenticationRequest(authenticationRequest, request, response); UriComponentsBuilder uriBuilder = UriComponentsBuilder .fromUriString(authenticationRequest.getAuthenticationRequestUri()); @@ -218,9 +222,7 @@ private void addParameter(String name, String value, UriComponentsBuilder builde } private void sendPost(HttpServletRequest request, HttpServletResponse response, - Saml2AuthenticationRequestContext context) throws IOException { - Saml2PostAuthenticationRequest authenticationRequest = this.authenticationRequestFactory - .createPostAuthenticationRequest(context); + Saml2PostAuthenticationRequest authenticationRequest) throws IOException { this.authenticationRequestRepository.saveAuthenticationRequest(authenticationRequest, request, response); String html = createSamlPostRequestFormData(authenticationRequest); response.setContentType(MediaType.TEXT_HTML_VALUE); @@ -269,4 +271,41 @@ private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest auth return html.toString(); } + private static class FactorySaml2AuthenticationRequestResolver implements Saml2AuthenticationRequestResolver { + + private final Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver; + + private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}"); + + private Saml2AuthenticationRequestFactory authenticationRequestFactory; + + FactorySaml2AuthenticationRequestResolver( + Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver, + Saml2AuthenticationRequestFactory authenticationRequestFactory) { + Assert.notNull(authenticationRequestContextResolver, "authenticationRequestContextResolver cannot be null"); + Assert.notNull(authenticationRequestFactory, "authenticationRequestFactory cannot be null"); + this.authenticationRequestContextResolver = authenticationRequestContextResolver; + this.authenticationRequestFactory = authenticationRequestFactory; + } + + @Override + public AbstractSaml2AuthenticationRequest resolve(HttpServletRequest request) { + MatchResult matcher = this.redirectMatcher.matcher(request); + if (!matcher.isMatch()) { + return null; + } + Saml2AuthenticationRequestContext context = this.authenticationRequestContextResolver.resolve(request); + if (context == null) { + return null; + } + Saml2MessageBinding binding = context.getRelyingPartyRegistration().getAssertingPartyDetails() + .getSingleSignOnServiceBinding(); + if (binding == Saml2MessageBinding.REDIRECT) { + return this.authenticationRequestFactory.createRedirectAuthenticationRequest(context); + } + return this.authenticationRequestFactory.createPostAuthenticationRequest(context); + } + + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java index 0a72ea5e7c9..f054a88759f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java @@ -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. @@ -35,7 +35,11 @@ * @author Shazin Sadakath * @author Josh Cummings * @since 5.4 + * @deprecated Use + * {@link org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver} + * instead */ +@Deprecated public final class DefaultSaml2AuthenticationRequestContextResolver implements Saml2AuthenticationRequestContextResolver { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java index 233d93b23d4..3ea8b4b47bf 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java @@ -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. @@ -28,7 +28,11 @@ * @author Shazin Sadakath * @author Josh Cummings * @since 5.4 + * @deprecated Use + * {@link org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver} + * instead */ +@Deprecated public interface Saml2AuthenticationRequestContextResolver { /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java index b100957d78b..cc963783b86 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java @@ -18,15 +18,14 @@ import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; import java.util.function.Function; import java.util.zip.Inflater; import java.util.zip.InflaterOutputStream; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.codec.CodecPolicy; -import org.apache.commons.codec.binary.Base64; - import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpMethod; import org.springframework.security.saml2.core.Saml2Error; @@ -49,9 +48,13 @@ */ public final class Saml2AuthenticationTokenConverter implements AuthenticationConverter { - private static Base64 BASE64 = new Base64(0, new byte[] { '\n' }, false, CodecPolicy.STRICT); + // MimeDecoder allows extra line-breaks as well as other non-alphabet values. + // This matches the behaviour of the commons-codec decoder. + private static final Base64.Decoder BASE64 = Base64.getMimeDecoder(); + + private static final Base64Checker BASE_64_CHECKER = new Base64Checker(); - private final Converter relyingPartyRegistrationResolver; + private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; private Function loader; @@ -67,24 +70,28 @@ public final class Saml2AuthenticationTokenConverter implements AuthenticationCo @Deprecated public Saml2AuthenticationTokenConverter( Converter relyingPartyRegistrationResolver) { - Assert.notNull(relyingPartyRegistrationResolver, "relyingPartyRegistrationResolver cannot be null"); - this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; - this.loader = new HttpSessionSaml2AuthenticationRequestRepository()::loadAuthenticationRequest; + this(adaptToResolver(relyingPartyRegistrationResolver)); } public Saml2AuthenticationTokenConverter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { - this(adaptToConverter(relyingPartyRegistrationResolver)); + Assert.notNull(relyingPartyRegistrationResolver, "relyingPartyRegistrationResolver cannot be null"); + this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; + this.loader = new HttpSessionSaml2AuthenticationRequestRepository()::loadAuthenticationRequest; } - private static Converter adaptToConverter( - RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + private static RelyingPartyRegistrationResolver adaptToResolver( + Converter relyingPartyRegistrationResolver) { Assert.notNull(relyingPartyRegistrationResolver, "relyingPartyRegistrationResolver cannot be null"); - return (request) -> relyingPartyRegistrationResolver.resolve(request, null); + return (request, relyingPartyRegistrationId) -> relyingPartyRegistrationResolver.convert(request); } @Override public Saml2AuthenticationToken convert(HttpServletRequest request) { - RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationResolver.convert(request); + AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request); + String relyingPartyRegistrationId = (authenticationRequest != null) + ? authenticationRequest.getRelyingPartyRegistrationId() : null; + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationResolver.resolve(request, + relyingPartyRegistrationId); if (relyingPartyRegistration == null) { return null; } @@ -94,7 +101,6 @@ public Saml2AuthenticationToken convert(HttpServletRequest request) { } byte[] b = samlDecode(saml2Response); saml2Response = inflateIfRequired(request, b); - AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request); return new Saml2AuthenticationToken(relyingPartyRegistration, saml2Response, authenticationRequest); } @@ -124,6 +130,7 @@ private String inflateIfRequired(HttpServletRequest request, byte[] b) { private byte[] samlDecode(String base64EncodedPayload) { try { + BASE_64_CHECKER.checkAcceptable(base64EncodedPayload); return BASE64.decode(base64EncodedPayload); } catch (Exception ex) { @@ -146,4 +153,58 @@ private String samlInflate(byte[] b) { } } + static class Base64Checker { + + private static final int[] values = genValueMapping(); + + Base64Checker() { + + } + + private static int[] genValueMapping() { + byte[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .getBytes(StandardCharsets.ISO_8859_1); + + int[] values = new int[256]; + Arrays.fill(values, -1); + for (int i = 0; i < alphabet.length; i++) { + values[alphabet[i] & 0xff] = i; + } + return values; + } + + boolean isAcceptable(String s) { + int goodChars = 0; + int lastGoodCharVal = -1; + + // count number of characters from Base64 alphabet + for (int i = 0; i < s.length(); i++) { + int val = values[0xff & s.charAt(i)]; + if (val != -1) { + lastGoodCharVal = val; + goodChars++; + } + } + + // in cases of an incomplete final chunk, ensure the unused bits are zero + switch (goodChars % 4) { + case 0: + return true; + case 2: + return (lastGoodCharVal & 0b1111) == 0; + case 3: + return (lastGoodCharVal & 0b11) == 0; + default: + return false; + } + } + + void checkAcceptable(String ins) { + if (!isAcceptable(ins)) { + throw new IllegalArgumentException("Unaccepted Encoding"); + } + } + + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java new file mode 100644 index 00000000000..b52f115c6b4 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiConsumer; + +import javax.servlet.http.HttpServletRequest; + +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; +import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller; +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml.saml2.core.impl.NameIDBuilder; +import org.w3c.dom.Element; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * For internal use only. Intended for consolidating common behavior related to minting a + * SAML 2.0 Authn Request. + */ +class OpenSamlAuthenticationRequestResolver { + + static { + OpenSamlInitializationService.initialize(); + } + + private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; + + private final AuthnRequestBuilder authnRequestBuilder; + + private final AuthnRequestMarshaller marshaller; + + private final IssuerBuilder issuerBuilder; + + private final NameIDBuilder nameIdBuilder; + + private RequestMatcher requestMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}"); + + private Converter relayStateResolver = (request) -> UUID.randomUUID().toString(); + + /** + * Construct a {@link OpenSamlAuthenticationRequestResolver} using the provided + * parameters + * @param relyingPartyRegistrationResolver a strategy for resolving the + * {@link RelyingPartyRegistration} from the {@link HttpServletRequest} + */ + OpenSamlAuthenticationRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + Assert.notNull(relyingPartyRegistrationResolver, "relyingPartyRegistrationResolver cannot be null"); + this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.marshaller = (AuthnRequestMarshaller) registry.getMarshallerFactory() + .getMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME); + Assert.notNull(this.marshaller, "logoutRequestMarshaller must be configured in OpenSAML"); + this.authnRequestBuilder = (AuthnRequestBuilder) XMLObjectProviderRegistrySupport.getBuilderFactory() + .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); + Assert.notNull(this.authnRequestBuilder, "authnRequestBuilder must be configured in OpenSAML"); + this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML"); + this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME); + Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML"); + } + + void setRelayStateResolver(Converter relayStateResolver) { + this.relayStateResolver = relayStateResolver; + } + + void setRequestMatcher(RequestMatcher requestMatcher) { + this.requestMatcher = requestMatcher; + } + + T resolve(HttpServletRequest request) { + return resolve(request, (registration, logoutRequest) -> { + }); + } + + T resolve(HttpServletRequest request, + BiConsumer authnRequestConsumer) { + RequestMatcher.MatchResult result = this.requestMatcher.matcher(request); + if (!result.isMatch()) { + return null; + } + String registrationId = result.getVariables().get("registrationId"); + RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId); + if (registration == null) { + return null; + } + AuthnRequest authnRequest = this.authnRequestBuilder.buildObject(); + authnRequest.setForceAuthn(Boolean.FALSE); + authnRequest.setIsPassive(Boolean.FALSE); + authnRequest.setProtocolBinding(registration.getAssertionConsumerServiceBinding().getUrn()); + Issuer iss = this.issuerBuilder.buildObject(); + iss.setValue(registration.getEntityId()); + authnRequest.setIssuer(iss); + authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation()); + authnRequestConsumer.accept(registration, authnRequest); + if (authnRequest.getID() == null) { + authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1)); + } + String relayState = this.relayStateResolver.convert(request); + Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleSignOnServiceBinding(); + if (binding == Saml2MessageBinding.POST) { + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { + OpenSamlSigningUtils.sign(authnRequest, registration); + } + String xml = serialize(authnRequest); + String encoded = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8)); + return (T) Saml2PostAuthenticationRequest.withRelyingPartyRegistration(registration).samlRequest(encoded) + .relayState(relayState).build(); + } + else { + String xml = serialize(authnRequest); + String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + Saml2RedirectAuthenticationRequest.Builder builder = Saml2RedirectAuthenticationRequest + .withRelyingPartyRegistration(registration).samlRequest(deflatedAndEncoded).relayState(relayState); + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { + Map parameters = OpenSamlSigningUtils.sign(registration) + .param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded) + .param(Saml2ParameterNames.RELAY_STATE, relayState).parameters(); + builder.sigAlg(parameters.get(Saml2ParameterNames.SIG_ALG)) + .signature(parameters.get(Saml2ParameterNames.SIGNATURE)); + } + return (T) builder.build(); + } + } + + private String serialize(AuthnRequest authnRequest) { + try { + Element element = this.marshaller.marshall(authnRequest); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java new file mode 100644 index 00000000000..df9d861065f --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; +import org.opensaml.security.SecurityException; +import org.opensaml.security.credential.BasicCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.SignatureSigningParametersResolver; +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager; +import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager; +import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; +import org.opensaml.xmlsec.signature.SignableXMLObject; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureSupport; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +/** + * Utility methods for signing SAML components with OpenSAML + * + * For internal use only. + * + * @author Josh Cummings + */ +final class OpenSamlSigningUtils { + + static String serialize(XMLObject object) { + try { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + Element element = marshaller.marshall(object); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + static O sign(O object, RelyingPartyRegistration relyingPartyRegistration) { + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + try { + SignatureSupport.signObject(object, parameters); + return object; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + static QueryParametersPartial sign(RelyingPartyRegistration registration) { + return new QueryParametersPartial(registration); + } + + private static SignatureSigningParameters resolveSigningParameters( + RelyingPartyRegistration relyingPartyRegistration) { + List credentials = resolveSigningCredentials(relyingPartyRegistration); + List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); + String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; + SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); + CriteriaSet criteria = new CriteriaSet(); + BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); + signingConfiguration.setSigningCredentials(credentials); + signingConfiguration.setSignatureAlgorithms(algorithms); + signingConfiguration.setSignatureReferenceDigestMethods(digests); + signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); + signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager()); + criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); + try { + SignatureSigningParameters parameters = resolver.resolveSingle(criteria); + Assert.notNull(parameters, "Failed to resolve any signing credential"); + return parameters; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private static NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() { + final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager(); + + namedManager.setUseDefaultManager(true); + final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager(); + + // Generator for X509Credentials + final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory(); + x509Factory.setEmitEntityCertificate(true); + x509Factory.setEmitEntityCertificateChain(true); + + defaultManager.registerFactory(x509Factory); + + return namedManager; + } + + private static List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { + List credentials = new ArrayList<>(); + for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { + X509Certificate certificate = x509Credential.getCertificate(); + PrivateKey privateKey = x509Credential.getPrivateKey(); + BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); + credential.setEntityId(relyingPartyRegistration.getEntityId()); + credential.setUsageType(UsageType.SIGNING); + credentials.add(credential); + } + return credentials; + } + + private OpenSamlSigningUtils() { + + } + + static class QueryParametersPartial { + + final RelyingPartyRegistration registration; + + final Map components = new LinkedHashMap<>(); + + QueryParametersPartial(RelyingPartyRegistration registration) { + this.registration = registration; + } + + QueryParametersPartial param(String key, String value) { + this.components.put(key, value); + return this; + } + + Map parameters() { + SignatureSigningParameters parameters = resolveSigningParameters(this.registration); + Credential credential = parameters.getSigningCredential(); + String algorithmUri = parameters.getSignatureAlgorithm(); + this.components.put("SigAlg", algorithmUri); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : this.components.entrySet()) { + builder.queryParam(component.getKey(), + UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + String queryString = builder.build(true).toString().substring(1); + try { + byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, + queryString.getBytes(StandardCharsets.UTF_8)); + String b64Signature = Saml2Utils.samlEncode(rawSignature); + this.components.put("Signature", b64Signature); + } + catch (SecurityException ex) { + throw new Saml2Exception(ex); + } + return this.components; + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java new file mode 100644 index 00000000000..fa84a0daeec --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.RequestAbstractType; +import org.opensaml.saml.saml2.core.StatusResponseType; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialResolver; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; + +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.web.util.UriUtils; + +/** + * Utility methods for verifying SAML component signatures with OpenSAML + * + * For internal use only. + * + * @author Josh Cummings + */ + +final class OpenSamlVerificationUtils { + + static VerifierPartial verifySignature(StatusResponseType object, RelyingPartyRegistration registration) { + return new VerifierPartial(object, registration); + } + + static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyRegistration registration) { + return new VerifierPartial(object, registration); + } + + private OpenSamlVerificationUtils() { + + } + + static class VerifierPartial { + + private final String id; + + private final CriteriaSet criteria; + + private final SignatureTrustEngine trustEngine; + + VerifierPartial(StatusResponseType object, RelyingPartyRegistration registration) { + this.id = object.getID(); + this.criteria = verificationCriteria(object.getIssuer()); + this.trustEngine = trustEngine(registration); + } + + VerifierPartial(RequestAbstractType object, RelyingPartyRegistration registration) { + this.id = object.getID(); + this.criteria = verificationCriteria(object.getIssuer()); + this.trustEngine = trustEngine(registration); + } + + Saml2ResponseValidatorResult redirect(HttpServletRequest request, String objectParameterName) { + RedirectSignature signature = new RedirectSignature(request, objectParameterName); + if (signature.getAlgorithm() == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Missing signature algorithm for object [" + this.id + "]")); + } + if (!signature.hasSignature()) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Missing signature for object [" + this.id + "]")); + } + Collection errors = new ArrayList<>(); + String algorithmUri = signature.getAlgorithm(); + try { + if (!this.trustEngine.validate(signature.getSignature(), signature.getContent(), algorithmUri, + this.criteria, null)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]: ")); + } + return Saml2ResponseValidatorResult.failure(errors); + } + + Saml2ResponseValidatorResult post(Signature signature) { + Collection errors = new ArrayList<>(); + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + try { + profileValidator.validate(signature); + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]: ")); + } + + try { + if (!this.trustEngine.validate(signature, this.criteria)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]: ")); + } + + return Saml2ResponseValidatorResult.failure(errors); + } + + private CriteriaSet verificationCriteria(Issuer issuer) { + CriteriaSet criteria = new CriteriaSet(); + criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue()))); + criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + return criteria; + } + + private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { + Set credentials = new HashSet<>(); + Collection keys = registration.getAssertingPartyDetails() + .getVerificationX509Credentials(); + for (Saml2X509Credential key : keys) { + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine(credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + } + + private static class RedirectSignature { + + private final HttpServletRequest request; + + private final String objectParameterName; + + RedirectSignature(HttpServletRequest request, String objectParameterName) { + this.request = request; + this.objectParameterName = objectParameterName; + } + + String getAlgorithm() { + return this.request.getParameter("SigAlg"); + } + + byte[] getContent() { + String query = String.format("%s=%s&SigAlg=%s", this.objectParameterName, + UriUtils.encode(this.request.getParameter(this.objectParameterName), + StandardCharsets.ISO_8859_1), + UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1)); + return query.getBytes(StandardCharsets.UTF_8); + } + + byte[] getSignature() { + return Saml2Utils.samlDecode(this.request.getParameter("Signature")); + } + + boolean hasSignature() { + return this.request.getParameter("Signature") != null; + } + + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2AuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2AuthenticationRequestResolver.java new file mode 100644 index 00000000000..c862f506d59 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2AuthenticationRequestResolver.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; + +/** + * A strategy for resolving a SAML 2.0 Authentication Request from the + * {@link HttpServletRequest}. + * + * @author Josh Cummings + * @since 5.7 + */ +public interface Saml2AuthenticationRequestResolver { + + T resolve(HttpServletRequest request); + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2Utils.java new file mode 100644 index 00000000000..019fab46c96 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2Utils.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.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.springframework.security.saml2.Saml2Exception; + +/** + * Utility methods for working with serialized SAML messages. + * + * For internal use only. + * + * @author Josh Cummings + */ +final class Saml2Utils { + + private Saml2Utils() { + } + + static String samlEncode(byte[] b) { + return Base64.getEncoder().encodeToString(b); + } + + static byte[] samlDecode(String s) { + return Base64.getMimeDecoder().decode(s); + } + + static byte[] samlDeflate(String s) { + try { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true)); + deflater.write(s.getBytes(StandardCharsets.UTF_8)); + deflater.finish(); + return b.toByteArray(); + } + catch (IOException ex) { + throw new Saml2Exception("Unable to deflate string", ex); + } + } + + static String samlInflate(byte[] b) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true)); + iout.write(b); + iout.finish(); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new Saml2Exception("Unable to inflate string", ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java index 5a5e64c6e37..db173a0f83d 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -31,10 +31,12 @@ import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.LogoutRequest; import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.SessionIndex; import org.opensaml.saml.saml2.core.impl.IssuerBuilder; import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder; import org.opensaml.saml.saml2.core.impl.LogoutRequestMarshaller; import org.opensaml.saml.saml2.core.impl.NameIDBuilder; +import org.opensaml.saml.saml2.core.impl.SessionIndexBuilder; import org.w3c.dom.Element; import org.springframework.security.core.Authentication; @@ -67,6 +69,8 @@ final class OpenSamlLogoutRequestResolver { private final NameIDBuilder nameIdBuilder; + private final SessionIndexBuilder sessionIndexBuilder; + private final LogoutRequestBuilder logoutRequestBuilder; private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; @@ -87,6 +91,9 @@ final class OpenSamlLogoutRequestResolver { Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML"); this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME); Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML"); + this.sessionIndexBuilder = (SessionIndexBuilder) registry.getBuilderFactory() + .getBuilder(SessionIndex.DEFAULT_ELEMENT_NAME); + Assert.notNull(this.sessionIndexBuilder, "sessionIndexBuilder must be configured in OpenSAML"); } /** @@ -111,6 +118,9 @@ Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentica 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(); @@ -119,6 +129,14 @@ Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentica NameID nameId = this.nameIdBuilder.buildObject(); nameId.setValue(authentication.getName()); logoutRequest.setNameID(nameId); + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) { + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + for (String index : principal.getSessionIndexes()) { + SessionIndex sessionIndex = this.sessionIndexBuilder.buildObject(); + sessionIndex.setSessionIndex(index); + logoutRequest.getSessionIndexes().add(sessionIndex); + } + } logoutRequestConsumer.accept(registration, logoutRequest); if (logoutRequest.getID() == null) { logoutRequest.setID("LR" + UUID.randomUUID()); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java index 935fb1febf5..7f60ad58819 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -132,9 +132,10 @@ Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentic if (registration == null) { return null; } - String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); - byte[] b = Saml2Utils.samlDecode(serialized); - LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b)); + if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) { + return null; + } + LogoutRequest logoutRequest = parse(extractSamlRequest(request)); LogoutResponse logoutResponse = this.logoutResponseBuilder.buildObject(); logoutResponse.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation()); Issuer issuer = this.issuerBuilder.buildObject(); @@ -187,8 +188,10 @@ private String getRegistrationId(Authentication authentication) { return null; } - private String inflateIfRequired(RelyingPartyRegistration registration, byte[] b) { - if (registration.getSingleLogoutServiceBinding() == Saml2MessageBinding.REDIRECT) { + private String extractSamlRequest(HttpServletRequest request) { + String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); + byte[] b = Saml2Utils.samlDecode(serialized); + if (Saml2MessageBindingUtils.isHttpRedirectBinding(request)) { return Saml2Utils.samlInflate(b); } return new String(b, StandardCharsets.UTF_8); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java index 99e88c9ae86..603fbac8ebd 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -17,8 +17,6 @@ package org.springframework.security.saml2.provider.service.web.authentication.logout; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.function.Function; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -53,7 +51,6 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; /** * A filter for handling logout requests in the form of a <saml2:LogoutRequest> sent @@ -120,7 +117,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } - if (!isCorrectBinding(request, registration)) { + 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; + } + + Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request); + if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) { this.logger.trace("Did not process logout request since used incorrect binding"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; @@ -129,13 +134,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) .samlRequest(serialized).relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE)) - .binding(registration.getSingleLogoutServiceBinding()) - .location(registration.getSingleLogoutServiceLocation()) + .binding(saml2MessageBinding).location(registration.getSingleLogoutServiceLocation()) .parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG, request.getParameter(Saml2ParameterNames.SIG_ALG))) .parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE, request.getParameter(Saml2ParameterNames.SIGNATURE))) - .build(); + .parametersQuery((params) -> request.getQueryString()).build(); Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(logoutRequest, registration, authentication); Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters); @@ -175,33 +179,14 @@ private String getRegistrationId(Authentication authentication) { return null; } - private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) { - Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding(); - if (requiredBinding == Saml2MessageBinding.POST) { - return "POST".equals(request.getMethod()); - } - return "GET".equals(request.getMethod()); - } - private void doRedirect(HttpServletRequest request, HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException { String location = logoutResponse.getResponseLocation(); - UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location); - addParameter(Saml2ParameterNames.SAML_RESPONSE, logoutResponse::getParameter, uriBuilder); - addParameter(Saml2ParameterNames.RELAY_STATE, logoutResponse::getParameter, uriBuilder); - addParameter(Saml2ParameterNames.SIG_ALG, logoutResponse::getParameter, uriBuilder); - addParameter(Saml2ParameterNames.SIGNATURE, logoutResponse::getParameter, uriBuilder); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location) + .query(logoutResponse.getParametersQuery()); this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString()); } - private void addParameter(String name, Function parameters, UriComponentsBuilder builder) { - Assert.hasText(name, "name cannot be empty or null"); - if (StringUtils.hasText(parameters.apply(name))) { - builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1), - UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1)); - } - } - private void doPost(HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException { String location = logoutResponse.getResponseLocation(); String saml = logoutResponse.getSamlResponse(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java index 83b4c8eccd4..2eea87b6dec 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -120,8 +120,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString()); return; } - if (!isCorrectBinding(request, registration)) { - this.logger.trace("Did not process logout request since used incorrect binding"); + 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; + } + + Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request); + if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) { + this.logger.trace("Did not process logout response since used incorrect binding"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } @@ -129,13 +137,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE); Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration) .samlResponse(serialized).relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE)) - .binding(registration.getSingleLogoutServiceBinding()) - .location(registration.getSingleLogoutServiceResponseLocation()) + .binding(saml2MessageBinding).location(registration.getSingleLogoutServiceResponseLocation()) .parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG, request.getParameter(Saml2ParameterNames.SIG_ALG))) .parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE, request.getParameter(Saml2ParameterNames.SIGNATURE))) - .build(); + .parametersQuery((params) -> request.getQueryString()).build(); Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(logoutResponse, logoutRequest, registration); Saml2LogoutValidatorResult result = this.logoutResponseValidator.validate(parameters); @@ -162,12 +169,4 @@ public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutReques this.logoutRequestRepository = logoutRequestRepository; } - private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) { - Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding(); - if (requiredBinding == Saml2MessageBinding.POST) { - return "POST".equals(request.getMethod()); - } - return "GET".equals(request.getMethod()); - } - } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2MessageBindingUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2MessageBindingUtils.java new file mode 100644 index 00000000000..813fbe42b1a --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2MessageBindingUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication.logout; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +/** + * Utility methods for working with {@link Saml2MessageBinding} + * + * For internal use only. + * + * @since 5.8 + */ +final class Saml2MessageBindingUtils { + + private Saml2MessageBindingUtils() { + } + + static Saml2MessageBinding resolveBinding(HttpServletRequest request) { + if (isHttpPostBinding(request)) { + return Saml2MessageBinding.POST; + } + else if (isHttpRedirectBinding(request)) { + return Saml2MessageBinding.REDIRECT; + } + throw new Saml2Exception("Unable to determine message binding from request."); + } + + private static boolean isSamlRequestResponse(HttpServletRequest request) { + return (request.getParameter(Saml2ParameterNames.SAML_REQUEST) != null + || request.getParameter(Saml2ParameterNames.SAML_RESPONSE) != null); + } + + static boolean isHttpRedirectBinding(HttpServletRequest request) { + return request != null && "GET".equalsIgnoreCase(request.getMethod()) && isSamlRequestResponse(request); + } + + static boolean isHttpPostBinding(HttpServletRequest request) { + return request != null && "POST".equalsIgnoreCase(request.getMethod()) && isSamlRequestResponse(request); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutSuccessHandler.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutSuccessHandler.java index 8d8b1f204f6..46f8d7a9915 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutSuccessHandler.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutSuccessHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -17,8 +17,6 @@ package org.springframework.security.saml2.provider.service.web.authentication.logout; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -28,7 +26,6 @@ import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; -import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.web.DefaultRedirectStrategy; @@ -38,7 +35,6 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; /** * A success handler for issuing a SAML 2.0 Logout Request to the the SAML 2.0 Asserting @@ -105,22 +101,11 @@ public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutReques private void doRedirect(HttpServletRequest request, HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException { String location = logoutRequest.getLocation(); - UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location); - addParameter(Saml2ParameterNames.SAML_REQUEST, logoutRequest::getParameter, uriBuilder); - addParameter(Saml2ParameterNames.RELAY_STATE, logoutRequest::getParameter, uriBuilder); - addParameter(Saml2ParameterNames.SIG_ALG, logoutRequest::getParameter, uriBuilder); - addParameter(Saml2ParameterNames.SIGNATURE, logoutRequest::getParameter, uriBuilder); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location) + .query(logoutRequest.getParametersQuery()); this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString()); } - private void addParameter(String name, Function parameters, UriComponentsBuilder builder) { - Assert.hasText(name, "name cannot be empty or null"); - if (StringUtils.hasText(parameters.apply(name))) { - builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1), - UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1)); - } - } - private void doPost(HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException { String location = logoutRequest.getLocation(); String saml = logoutRequest.getSamlRequest(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java index fc0c71aad85..95046bc3a18 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java @@ -44,7 +44,7 @@ static String samlEncode(byte[] b) { } static byte[] samlDecode(String s) { - return Base64.getDecoder().decode(s); + return Base64.getMimeDecoder().decode(s); } static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/opensaml3Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml3AuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/opensaml3Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml3AuthenticationRequestResolver.java new file mode 100644 index 00000000000..b95aaa97cc3 --- /dev/null +++ b/saml2/saml2-service-provider/src/opensaml3Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml3AuthenticationRequestResolver.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import java.time.Clock; +import java.util.function.Consumer; + +import javax.servlet.http.HttpServletRequest; + +import org.joda.time.DateTime; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.LogoutRequest; + +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; +import org.springframework.util.Assert; + +/** + * A strategy for resolving a SAML 2.0 Authentication Request from the + * {@link HttpServletRequest} using OpenSAML. + * + * @author Josh Cummings + * @since 5.7 + * @deprecated OpenSAML 3 has reached end-of-life so this version is no longer recommended + */ +@Deprecated +public final class OpenSaml3AuthenticationRequestResolver implements Saml2AuthenticationRequestResolver { + + private final OpenSamlAuthenticationRequestResolver authnRequestResolver; + + private Consumer contextConsumer = (parameters) -> { + }; + + private Clock clock = Clock.systemUTC(); + + /** + * Construct a {@link OpenSaml3AuthenticationRequestResolver} + */ + public OpenSaml3AuthenticationRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + this.authnRequestResolver = new OpenSamlAuthenticationRequestResolver(relyingPartyRegistrationResolver); + } + + @Override + public T resolve(HttpServletRequest request) { + return this.authnRequestResolver.resolve(request, (registration, authnRequest) -> { + authnRequest.setIssueInstant(new DateTime(this.clock.millis())); + this.contextConsumer.accept(new AuthnRequestContext(request, registration, authnRequest)); + }); + } + + /** + * Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest} + * @param contextConsumer a consumer that accepts an {@link AuthnRequestContext} + */ + public void setAuthnRequestCustomizer(Consumer contextConsumer) { + Assert.notNull(contextConsumer, "contextConsumer cannot be null"); + this.contextConsumer = contextConsumer; + } + + /** + * Use this {@link Clock} for generating the issued {@link DateTime} + * @param clock the {@link Clock} to use + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "clock must not be null"); + this.clock = clock; + } + + public static final class AuthnRequestContext { + + private final HttpServletRequest request; + + private final RelyingPartyRegistration registration; + + private final AuthnRequest authnRequest; + + public AuthnRequestContext(HttpServletRequest request, RelyingPartyRegistration registration, + AuthnRequest authnRequest) { + this.request = request; + this.registration = registration; + this.authnRequest = authnRequest; + } + + public HttpServletRequest getRequest() { + return this.request; + } + + public RelyingPartyRegistration getRelyingPartyRegistration() { + return this.registration; + } + + public AuthnRequest getAuthnRequest() { + return this.authnRequest; + } + + } + +} diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java index 99e5d225b1a..57a7e7247b3 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java @@ -47,7 +47,9 @@ public void resolveWhenCustomParametersConsumerThenUses() { 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); diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java index 2e5a4a0a435..628d2401e44 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java @@ -53,7 +53,10 @@ public void resolveWhenCustomParametersConsumerThenUses() { Consumer 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, diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java index cac0c2a3edd..31acccfa743 100644 --- a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -37,6 +37,7 @@ import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.schema.XSAny; import org.opensaml.core.xml.schema.XSBoolean; import org.opensaml.core.xml.schema.XSBooleanValue; @@ -57,12 +58,16 @@ import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.Condition; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.OneTimeUse; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.core.SubjectConfirmation; +import org.opensaml.saml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml.saml2.core.impl.AuthnRequestUnmarshaller; import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; import org.opensaml.saml.saml2.encryption.Decrypter; import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; @@ -84,6 +89,7 @@ import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -139,6 +145,13 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv private final ResponseUnmarshaller responseUnmarshaller; + private static final AuthnRequestUnmarshaller authnRequestUnmarshaller; + static { + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + authnRequestUnmarshaller = (AuthnRequestUnmarshaller) registry.getUnmarshallerFactory() + .getUnmarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME); + } + private final ParserPool parserPool; private final Converter responseSignatureValidator = createDefaultResponseSignatureValidator(); @@ -364,6 +377,10 @@ public static Converter createDefau response.getID()); result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, message)); } + + String inResponseTo = response.getInResponseTo(); + result = result.concat(validateInResponseTo(token.getAuthenticationRequest(), inResponseTo)); + String issuer = response.getIssuer().getValue(); String destination = response.getDestination(); String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation(); @@ -379,13 +396,34 @@ public static Converter createDefau result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message)); } if (response.getAssertions().isEmpty()) { - throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, - "No assertions found in response.", null); + result = result.concat( + new Saml2Error(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response.")); } return result; }; } + private static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest, + String inResponseTo) { + if (!StringUtils.hasText(inResponseTo)) { + return Saml2ResponseValidatorResult.success(); + } + AuthnRequest request = parseRequest(storedRequest); + if (request == null) { + String message = "The response contained an InResponseTo attribute [" + inResponseTo + "]" + + " but no saved authentication request was found"; + return Saml2ResponseValidatorResult + .failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message)); + } + if (!inResponseTo.equals(request.getID())) { + String message = "The InResponseTo attribute [" + inResponseTo + "] does not match the ID of the " + + "authentication request [" + request.getID() + "]"; + return Saml2ResponseValidatorResult + .failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message)); + } + return Saml2ResponseValidatorResult.success(); + } + /** * Construct a default strategy for validating each SAML 2.0 Assertion and associated * {@link Authentication} token @@ -425,7 +463,9 @@ public static Converter createDefaultRespons Assertion assertion = CollectionUtils.firstElement(response.getAssertions()); String username = assertion.getSubject().getNameID().getValue(); Map> attributes = getAssertionAttributes(assertion); - DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes); + List sessionIndexes = getSessionIndexes(assertion); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes, + sessionIndexes); String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId(); principal.setRelyingPartyRegistrationId(registrationId); return new Saml2Authentication(principal, token.getSaml2Response(), @@ -444,7 +484,7 @@ public Authentication authenticate(Authentication authentication) throws Authent try { Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; String serializedResponse = token.getSaml2Response(); - Response response = parse(serializedResponse); + Response response = parseResponse(serializedResponse); process(token, response); AbstractAuthenticationToken authenticationResponse = this.responseAuthenticationConverter .convert(new ResponseToken(response, token)); @@ -466,7 +506,7 @@ public boolean supports(Class authentication) { return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication); } - private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException { + private Response parseResponse(String response) throws Saml2Exception, Saml2AuthenticationException { try { Document document = this.parserPool .parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8))); @@ -488,6 +528,10 @@ private void process(Saml2AuthenticationToken token, Response response) { if (responseSigned) { this.responseElementsDecrypter.accept(responseToken); } + else if (!response.getEncryptedAssertions().isEmpty()) { + result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Did not decrypt response [" + response.getID() + "] since it is not signed")); + } result = result.concat(this.responseValidator.convert(responseToken)); boolean allAssertionsSigned = true; for (Assertion assertion : response.getAssertions()) { @@ -502,10 +546,10 @@ private void process(Saml2AuthenticationToken token, Response response) { if (!responseSigned && !allAssertionsSigned) { String description = "Either the response or one of the assertions is unsigned. " + "Please either sign the response or all of the assertions."; - throw createAuthenticationException(Saml2ErrorCodes.INVALID_SIGNATURE, description, null); + result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description)); } Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions()); - if (!hasName(firstAssertion)) { + if (firstAssertion != null && !hasName(firstAssertion)) { Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Assertion [" + firstAssertion.getID() + "] is missing a subject"); result = result.concat(error); @@ -617,6 +661,14 @@ private static Map> getAssertionAttributes(Assertion assert return attributeMap; } + private static List getSessionIndexes(Assertion assertion) { + List sessionIndexes = new ArrayList<>(); + for (AuthnStatement statement : assertion.getAuthnStatements()) { + sessionIndexes.add(statement.getSessionIndex()); + } + return sessionIndexes; + } + private static Object getXmlObjectValue(XMLObject xmlObject) { if (xmlObject instanceof XSAny) { return ((XSAny) xmlObject).getTextContent(); @@ -637,7 +689,7 @@ private static Object getXmlObjectValue(XMLObject xmlObject) { if (xmlObject instanceof XSDateTime) { return ((XSDateTime) xmlObject).getValue(); } - return null; + return xmlObject; } private static Saml2AuthenticationException createAuthenticationException(String code, String message, @@ -672,11 +724,17 @@ private static Converter createAss private static ValidationContext createValidationContext(AssertionToken assertionToken, Consumer> paramsConsumer) { - RelyingPartyRegistration relyingPartyRegistration = assertionToken.token.getRelyingPartyRegistration(); + Saml2AuthenticationToken token = assertionToken.token; + RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration(); String audience = relyingPartyRegistration.getEntityId(); String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation(); String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyDetails().getEntityId(); Map params = new HashMap<>(); + Assertion assertion = assertionToken.getAssertion(); + if (assertionContainsInResponseTo(assertion)) { + String requestId = getAuthnRequestId(token.getAuthenticationRequest()); + params.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO, requestId); + } params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience)); params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient)); params.put(SAML2AssertionValidationParameters.VALID_ISSUERS, Collections.singleton(assertingPartyEntityId)); @@ -684,6 +742,56 @@ private static ValidationContext createValidationContext(AssertionToken assertio return new ValidationContext(params); } + private static boolean assertionContainsInResponseTo(Assertion assertion) { + if (assertion.getSubject() == null) { + return false; + } + for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) { + SubjectConfirmationData confirmationData = confirmation.getSubjectConfirmationData(); + if (confirmationData == null) { + continue; + } + if (StringUtils.hasText(confirmationData.getInResponseTo())) { + return true; + } + } + return false; + } + + private static String getAuthnRequestId(AbstractSaml2AuthenticationRequest serialized) { + AuthnRequest request = parseRequest(serialized); + if (request == null) { + return null; + } + return request.getID(); + } + + private static AuthnRequest parseRequest(AbstractSaml2AuthenticationRequest request) { + if (request == null) { + return null; + } + String samlRequest = request.getSamlRequest(); + if (!StringUtils.hasText(samlRequest)) { + return null; + } + if (request.getBinding() == Saml2MessageBinding.REDIRECT) { + samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest)); + } + else { + samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8); + } + try { + Document document = XMLObjectProviderRegistrySupport.getParserPool() + .parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (AuthnRequest) authnRequestUnmarshaller.unmarshall(element); + } + catch (Exception ex) { + String message = "Failed to deserialize associated authentication request [" + ex.getMessage() + "]"; + throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_REQUEST_DATA, message, ex); + } + } + private static class SAML20AssertionValidators { private static final Collection conditions = new ArrayList<>(); @@ -718,13 +826,6 @@ protected ValidationResult validateAddress(SubjectConfirmation confirmation, Ass // applications should validate their own addresses - gh-7514 return ValidationResult.VALID; } - - @Override - protected ValidationResult validateInResponseTo(SubjectConfirmation confirmation, Assertion assertion, - ValidationContext context, boolean required) { - // applications should validate their own in response to - return ValidationResult.VALID; - } }); } diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java index dcfa1cfdbc1..908beed7fc5 100644 --- a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java @@ -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. @@ -27,8 +27,10 @@ import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameIDPolicy; import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; import org.opensaml.saml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder; import org.springframework.core.convert.converter.Converter; import org.springframework.security.saml2.core.OpenSamlInitializationService; @@ -45,7 +47,11 @@ * * @author Josh Cummings * @since 5.5 + * @deprecated Use + * {@link org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver} + * instead */ +@Deprecated public final class OpenSaml4AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { static { @@ -56,6 +62,8 @@ public final class OpenSaml4AuthenticationRequestFactory implements Saml2Authent private final IssuerBuilder issuerBuilder; + private final NameIDPolicyBuilder nameIdPolicyBuilder; + private Clock clock = Clock.systemUTC(); private Converter authenticationRequestContextConverter; @@ -69,6 +77,8 @@ public OpenSaml4AuthenticationRequestFactory() { this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory() .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + this.nameIdPolicyBuilder = (NameIDPolicyBuilder) registry.getBuilderFactory() + .getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME); } /** @@ -152,6 +162,9 @@ private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext contex auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); } auth.setProtocolBinding(protocolBinding); + if (auth.getNameIDPolicy() == null) { + setNameIdPolicy(auth, context.getRelyingPartyRegistration()); + } Issuer iss = this.issuerBuilder.buildObject(); iss.setValue(issuer); auth.setIssuer(iss); @@ -160,6 +173,15 @@ private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext contex return auth; } + private void setNameIdPolicy(AuthnRequest authnRequest, RelyingPartyRegistration registration) { + if (!StringUtils.hasText(registration.getNameIdFormat())) { + return; + } + NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject(); + nameIdPolicy.setFormat(registration.getNameIdFormat()); + authnRequest.setNameIDPolicy(nameIdPolicy); + } + /** * Set the strategy for building an {@link AuthnRequest} from a given context * @param authenticationRequestContextConverter the conversion strategy to use diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java new file mode 100644 index 00000000000..937fe13b8b9 --- /dev/null +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import java.time.Clock; +import java.time.Instant; +import java.util.function.Consumer; + +import javax.servlet.http.HttpServletRequest; + +import org.opensaml.saml.saml2.core.AuthnRequest; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * A strategy for resolving a SAML 2.0 Authentication Request from the + * {@link HttpServletRequest} using OpenSAML. + * + * @author Josh Cummings + * @since 5.7 + */ +public final class OpenSaml4AuthenticationRequestResolver implements Saml2AuthenticationRequestResolver { + + private final OpenSamlAuthenticationRequestResolver authnRequestResolver; + + private Consumer contextConsumer = (parameters) -> { + }; + + private Clock clock = Clock.systemUTC(); + + /** + * Construct a {@link OpenSaml4AuthenticationRequestResolver} + */ + public OpenSaml4AuthenticationRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + this.authnRequestResolver = new OpenSamlAuthenticationRequestResolver(relyingPartyRegistrationResolver); + } + + @Override + public T resolve(HttpServletRequest request) { + return this.authnRequestResolver.resolve(request, (registration, authnRequest) -> { + authnRequest.setIssueInstant(Instant.now(this.clock)); + this.contextConsumer.accept(new AuthnRequestContext(request, registration, authnRequest)); + }); + } + + /** + * Set a {@link Consumer} for modifying the OpenSAML {@link AuthnRequest} + * @param contextConsumer a consumer that accepts an {@link AuthnRequestContext} + */ + public void setAuthnRequestCustomizer(Consumer contextConsumer) { + Assert.notNull(contextConsumer, "contextConsumer cannot be null"); + this.contextConsumer = contextConsumer; + } + + /** + * Set the {@link RequestMatcher} to use for setting the + * {@link OpenSamlAuthenticationRequestResolver#setRequestMatcher(RequestMatcher)} + * (RequestMatcher)} + * @param requestMatcher the {@link RequestMatcher} to identify authentication + * requests. + * @since 5.8 + */ + public void setRequestMatcher(RequestMatcher requestMatcher) { + Assert.notNull(requestMatcher, "requestMatcher cannot be null"); + this.authnRequestResolver.setRequestMatcher(requestMatcher); + } + + /** + * Use this {@link Clock} for generating the issued {@link Instant} + * @param clock the {@link Clock} to use + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "clock must not be null"); + this.clock = clock; + } + + /** + * Use this {@link Converter} to compute the RelayState + * @param relayStateResolver the {@link Converter} to use + * @since 5.8 + */ + public void setRelayStateResolver(Converter relayStateResolver) { + Assert.notNull(relayStateResolver, "relayStateResolver cannot be null"); + this.authnRequestResolver.setRelayStateResolver(relayStateResolver); + } + + public static final class AuthnRequestContext { + + private final HttpServletRequest request; + + private final RelyingPartyRegistration registration; + + private final AuthnRequest authnRequest; + + public AuthnRequestContext(HttpServletRequest request, RelyingPartyRegistration registration, + AuthnRequest authnRequest) { + this.request = request; + this.registration = registration; + this.authnRequest = authnRequest; + } + + public HttpServletRequest getRequest() { + return this.request; + } + + public RelyingPartyRegistration getRelyingPartyRegistration() { + return this.registration; + } + + public AuthnRequest getAuthnRequest() { + return this.authnRequest; + } + + } + +} diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java index 400d196b725..6a7eb1ff0c5 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,6 +19,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -39,12 +40,14 @@ import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.core.xml.schema.XSDateTime; import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder; +import org.opensaml.saml.common.SignableSAMLObject; import org.opensaml.saml.common.assertion.ValidationContext; import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeValue; +import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; @@ -71,7 +74,9 @@ import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; +import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSamlObjects.CustomOpenSamlObject; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; import org.springframework.util.StringUtils; @@ -143,9 +148,7 @@ public void authenticateWhenXmlErrorThenThrowAuthenticationException() { public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID); response.getAssertions().add(assertion()); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) .satisfies(errorOf(Saml2ErrorCodes.INVALID_DESTINATION)); @@ -175,9 +178,7 @@ public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationExcept Assertion assertion = assertion(); assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData() .setNotOnOrAfter(Instant.now().minus(Duration.ofDays(3))); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -189,9 +190,7 @@ public void authenticateWhenMissingSubjectThenThrowAuthenticationException() { Response response = response(); Assertion assertion = assertion(); assertion.setSubject(null); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -203,9 +202,7 @@ public void authenticateWhenUsernameMissingThenThrowAuthenticationException() { Response response = response(); Assertion assertion = assertion(); assertion.getSubject().getNameID().setValue(null); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -218,22 +215,123 @@ public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() { Assertion assertion = assertion(); assertion.getSubject().getSubjectConfirmations() .forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10")); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); this.provider.authenticate(token); } + @Test + public void evaluateInResponseToSucceedsWhenInResponseToInResponseAndAssertionsMatchRequestID() { + Response response = response(); + response.setInResponseTo("SAML2"); + response.getAssertions().add(signed(assertion("SAML2"))); + response.getAssertions().add(signed(assertion("SAML2"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + this.provider.authenticate(token); + } + + @Test + public void evaluateInResponseToSucceedsWhenInResponseToInAssertionOnlyMatchRequestID() { + Response response = response(); + response.getAssertions().add(signed(assertion())); + response.getAssertions().add(signed(assertion("SAML2"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + this.provider.authenticate(token); + } + + @Test + public void evaluateInResponseToFailsWhenInResponseToInAssertionOnlyAndCorruptedStoredRequest() { + Response response = response(); + response.getAssertions().add(signed(assertion())); + response.getAssertions().add(signed(assertion("SAML2"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, true); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("malformed_request_data"); + } + + @Test + public void evaluateInResponseToFailsWhenInResponseToInAssertionMismatchWithRequestID() { + Response response = response(); + response.setInResponseTo("SAML2"); + response.getAssertions().add(signed(assertion("SAML2"))); + response.getAssertions().add(signed(assertion("BAD"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("invalid_assertion"); + } + + @Test + public void evaluateInResponseToFailsWhenInResponseToInAssertionOnlyAndMismatchWithRequestID() { + Response response = response(); + response.getAssertions().add(signed(assertion())); + response.getAssertions().add(signed(assertion("BAD"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("invalid_assertion"); + } + + @Test + public void evaluateInResponseToFailsWhenInResponseInToResponseMismatchWithRequestID() { + Response response = response(); + response.setInResponseTo("BAD"); + response.getAssertions().add(signed(assertion("SAML2"))); + response.getAssertions().add(signed(assertion("SAML2"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("invalid_in_response_to"); + } + + @Test + public void evaluateInResponseToFailsWhenInResponseInToResponseAndCorruptedStoredRequest() { + Response response = response(); + response.setInResponseTo("SAML2"); + response.getAssertions().add(signed(assertion())); + response.getAssertions().add(signed(assertion())); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, true); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("malformed_request_data"); + } + + @Test + public void evaluateInResponseToFailsWhenInResponseToInResponseButNoSavedRequest() { + Response response = response(); + response.setInResponseTo("BAD"); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("invalid_in_response_to"); + } + + @Test + public void evaluateInResponseToSucceedsWhenNoInResponseToInResponseOrAssertions() { + Response response = response(); + response.getAssertions().add(signed(assertion())); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + this.provider.authenticate(token); + } + @Test public void authenticateWhenAssertionContainsAttributesThenItSucceeds() { Response response = response(); Assertion assertion = assertion(); List attributes = attributeStatements(); assertion.getAttributeStatements().addAll(attributes); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); Authentication authentication = this.provider.authenticate(token); Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); @@ -247,6 +345,25 @@ public void authenticateWhenAssertionContainsAttributesThenItSucceeds() { expected.put("registeredDate", Collections.singletonList(registeredDate)); assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe"); assertThat(principal.getAttributes()).isEqualTo(expected); + assertThat(principal.getSessionIndexes()).contains("session-index"); + } + + @Test + public void authenticateWhenAssertionContainsCustomAttributesThenItSucceeds() { + Response response = response(); + Assertion assertion = assertion(); + AttributeStatement attribute = TestOpenSamlObjects.customAttributeStatement("Address", + TestCustomOpenSamlObjects.instance()); + assertion.getAttributeStatements().add(attribute); + response.getAssertions().add(signed(assertion)); + Saml2AuthenticationToken token = token(response, verifying(registration())); + Authentication authentication = this.provider.authenticate(token); + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + CustomOpenSamlObject address = (CustomOpenSamlObject) principal.getAttribute("Address").get(0); + assertThat(address.getStreet()).isEqualTo("Test Street"); + assertThat(address.getStreetNumber()).isEqualTo("1"); + assertThat(address.getZIP()).isEqualTo("11111"); + assertThat(address.getCity()).isEqualTo("Test City"); } @Test @@ -255,12 +372,10 @@ public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() { EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, decrypting(registration())); + Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) - .satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE)); + .satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE, "Did not decrypt response")); } @Test @@ -271,9 +386,7 @@ public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() { EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion, TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration()))); this.provider.authenticate(token); } @@ -283,9 +396,7 @@ public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceed EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration()))); this.provider.authenticate(token); } @@ -298,9 +409,7 @@ public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() { TestSaml2X509Credentials.assertingPartyEncryptingCredential()); assertion.getSubject().setNameID(null); assertion.getSubject().setEncryptedID(encryptedID); - response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); this.provider.authenticate(token); } @@ -315,9 +424,7 @@ public void authenticateWhenEncryptedAttributeThenDecrypts() { statement.getEncryptedAttributes().add(attribute); assertion.getAttributeStatements().add(statement); response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration()))); Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token); Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); assertThat(principal.getAttribute("name")).containsExactly("value"); @@ -329,9 +436,7 @@ public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationExcep EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) .satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData")); @@ -343,9 +448,7 @@ public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationExcepti EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, registration() + Saml2AuthenticationToken token = token(signed(response), registration() .decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyPrivateCredential()))); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -358,9 +461,7 @@ public void authenticateWhenAuthenticationHasDetailsThenSucceeds() { Assertion assertion = assertion(); assertion.getSubject().getSubjectConfirmations() .forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10")); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); token.setDetails("some-details"); Authentication authentication = this.provider.authenticate(token); @@ -375,9 +476,7 @@ public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOE EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion, TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + Saml2AuthenticationToken token = token(signed(response), decrypting(verifying(registration()))); Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token); // the following code will throw an exception if authentication isn't serializable ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024); @@ -412,9 +511,7 @@ public void authenticateWhenDelegatingToDefaultAssertionValidatorThenUses() { OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME); assertion.getConditions().getConditions().add(oneTimeUse); response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - ASSERTING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); // @formatter:off assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class) @@ -436,9 +533,7 @@ public void authenticateWhenCustomAssertionValidatorThenUses() { Response response = response(); Assertion assertion = assertion(); response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - ASSERTING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); given(validator.convert(any(OpenSaml4AuthenticationProvider.AssertionToken.class))) .willReturn(Saml2ResponseValidatorResult.success()); provider.authenticate(token); @@ -455,9 +550,7 @@ public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillCh RELYING_PARTY_ENTITY_ID); // broken // signature response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - ASSERTING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); // @formatter:off assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> provider.authenticate(token)) @@ -476,9 +569,7 @@ public void authenticateWhenValidationContextCustomizedThenUsers() { OpenSaml4AuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context)); Response response = response(); Assertion assertion = assertion(); - response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - ASSERTING_PARTY_ENTITY_ID); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); // @formatter:off assertThatExceptionOfType(Saml2AuthenticationException.class) @@ -550,13 +641,12 @@ public void setAssertionElementsDecrypterWhenNullThenIllegalArgument() { public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() { Response response = response(); Assertion assertion = assertion(); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject()); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); Saml2AuthenticationToken token = token(response, verifying(registration())); - this.provider.setResponseElementsDecrypter((tuple) -> tuple.getResponse().getAssertions().add(assertion)); + this.provider + .setResponseElementsDecrypter((tuple) -> tuple.getResponse().getAssertions().add(signed(assertion))); Authentication authentication = this.provider.authenticate(token); assertThat(authentication.getName()).isEqualTo("test@saml.user"); } @@ -568,9 +658,7 @@ public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertio EncryptedID id = new EncryptedIDBuilder().buildObject(); id.setEncryptedData(new EncryptedDataBuilder().buildObject()); assertion.getSubject().setEncryptedID(id); - TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), - RELYING_PARTY_ENTITY_ID); - response.getAssertions().add(assertion); + response.getAssertions().add(signed(assertion)); Saml2AuthenticationToken token = token(response, verifying(registration())); this.provider.setAssertionElementsDecrypter((tuple) -> { NameID name = new NameIDBuilder().buildObject(); @@ -619,9 +707,7 @@ public void authenticateWhenCustomResponseValidatorThenUses() { Response response = response(); Assertion assertion = assertion(); response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - ASSERTING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); given(validator.convert(any(OpenSaml4AuthenticationProvider.ResponseToken.class))) .willReturn(Saml2ResponseValidatorResult.success()); provider.authenticate(token); @@ -635,9 +721,7 @@ public void authenticateWhenAssertionIssuerNotValidThenFailsWithInvalidIssuer() Assertion assertion = assertion(); assertion.setIssuer(TestOpenSamlObjects.issuer("https://invalid.idp.test/saml2/idp")); response.getAssertions().add(assertion); - TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), - ASSERTING_PARTY_ENTITY_ID); - Saml2AuthenticationToken token = token(response, verifying(registration())); + Saml2AuthenticationToken token = token(signed(response), verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token)) .withMessageContaining("did not match any valid issuers"); } @@ -682,13 +766,27 @@ private Response response(String destination, String issuerEntityId) { return response; } - private Assertion assertion() { + private AuthnRequest request() { + AuthnRequest request = TestOpenSamlObjects.authnRequest(); + return request; + } + + private String serializedRequest(AuthnRequest request, Saml2MessageBinding binding) { + String xml = serialize(request); + return (binding == Saml2MessageBinding.POST) ? Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8)) + : Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + } + + private Assertion assertion(String inResponseTo) { Assertion assertion = TestOpenSamlObjects.assertion(); assertion.setIssueInstant(Instant.now()); for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) { SubjectConfirmationData data = confirmation.getSubjectConfirmationData(); data.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000))); data.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000))); + if (StringUtils.hasText(inResponseTo)) { + data.setInResponseTo(inResponseTo); + } } Conditions conditions = assertion.getConditions(); conditions.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000))); @@ -696,6 +794,16 @@ private Assertion assertion() { return assertion; } + private Assertion assertion() { + return assertion(null); + } + + private T signed(T toSign) { + TestOpenSamlObjects.signed(toSign, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + return toSign; + } + private List attributeStatements() { List attributeStatements = TestOpenSamlObjects.attributeStatements(); AttributeBuilder attributeBuilder = new AttributeBuilder(); @@ -719,6 +827,27 @@ private Saml2AuthenticationToken token(Response response, RelyingPartyRegistrati return new Saml2AuthenticationToken(registration.build(), serialize(response)); } + private Saml2AuthenticationToken token(Response response, RelyingPartyRegistration.Builder registration, + AbstractSaml2AuthenticationRequest authenticationRequest) { + return new Saml2AuthenticationToken(registration.build(), serialize(response), authenticationRequest); + } + + private AbstractSaml2AuthenticationRequest mockedStoredAuthenticationRequest(String requestId, + Saml2MessageBinding binding, boolean corruptRequestString) { + AuthnRequest request = request(); + if (requestId != null) { + request.setID(requestId); + } + String serializedRequest = serializedRequest(request, binding); + if (corruptRequestString) { + serializedRequest = serializedRequest.substring(2, serializedRequest.length() - 2); + } + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mock(AbstractSaml2AuthenticationRequest.class); + given(mockAuthenticationRequest.getSamlRequest()).willReturn(serializedRequest); + given(mockAuthenticationRequest.getBinding()).willReturn(binding); + return mockAuthenticationRequest; + } + private RelyingPartyRegistration.Builder registration() { return TestRelyingPartyRegistrations.noCredentials().entityId(RELYING_PARTY_ENTITY_ID) .assertionConsumerServiceLocation(DESTINATION) diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java index 84c415ebe56..0aced67097b 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java @@ -242,6 +242,18 @@ public void createRedirectAuthenticationRequestWhenSHA1SignRequestThenSignatureI assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); } + @Test + public void createAuthenticationRequestWhenSetNameIDPolicyThenReturnsCorrectNameIDPolicy() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().nameIdFormat("format").build(); + this.context = this.contextBuilder.relayState("Relay State Value").relyingPartyRegistration(registration) + .build(); + AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST); + assertThat(authn.getNameIDPolicy()).isNotNull(); + assertThat(authn.getNameIDPolicy().getAllowCreate()).isFalse(); + assertThat(authn.getNameIDPolicy().getFormat()).isEqualTo("format"); + assertThat(authn.getNameIDPolicy().getSPNameQualifier()).isNull(); + } + private AuthnRequest authnRequest() { AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest(); authnRequest.setIssueInstant(Instant.now()); diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolverTests.java new file mode 100644 index 00000000000..ac491d86beb --- /dev/null +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolverTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class OpenSaml4AuthenticationRequestResolverTests { + + MockHttpServletRequest request; + + RelyingPartyRegistration registration; + + @BeforeEach + void setup() { + this.request = givenRequest("/saml2/authenticate/registration-id"); + this.registration = TestRelyingPartyRegistrations.full().build(); + } + + @Test + void resolveWhenRedirectThenSaml2RedirectAuthenticationRequest() { + RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class); + given(relyingParties.resolve(any(), any())).willReturn(this.registration); + OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties); + Saml2RedirectAuthenticationRequest authnRequest = resolver.resolve(this.request); + assertThat(authnRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(authnRequest.getAuthenticationRequestUri()) + .isEqualTo(this.registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + } + + @Test + void resolveWhenPostThenSaml2PostAuthenticationRequest() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full() + .assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build(); + RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class); + given(relyingParties.resolve(any(), any())).willReturn(registration); + OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties); + Saml2PostAuthenticationRequest authnRequest = resolver.resolve(this.request); + assertThat(authnRequest.getBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(authnRequest.getAuthenticationRequestUri()) + .isEqualTo(this.registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + } + + @Test + void resolveWhenCustomRelayStateThenUses() { + RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class); + given(relyingParties.resolve(any(), any())).willReturn(this.registration); + Converter relayState = mock(Converter.class); + given(relayState.convert(any())).willReturn("state"); + OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties); + resolver.setRelayStateResolver(relayState); + Saml2RedirectAuthenticationRequest authnRequest = resolver.resolve(this.request); + assertThat(authnRequest.getRelayState()).isEqualTo("state"); + verify(relayState).convert(any()); + } + + @Test + void resolveWhenCustomAuthenticationUrlTHenUses() { + RelyingPartyRegistrationResolver relyingParties = mock(RelyingPartyRegistrationResolver.class); + given(relyingParties.resolve(any(), any())).willReturn(this.registration); + OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingParties); + resolver.setRequestMatcher(new AntPathRequestMatcher("/custom/authentication/{registrationId}")); + Saml2RedirectAuthenticationRequest authnRequest = resolver + .resolve(givenRequest("/custom/authentication/registration-id")); + + assertThat(authnRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(authnRequest.getAuthenticationRequestUri()) + .isEqualTo(this.registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + + } + + private MockHttpServletRequest givenRequest(String path) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath(path); + return request; + } + +} diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java index 6ea35b47161..4d7c5c266e7 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java @@ -47,7 +47,9 @@ public void resolveWhenCustomParametersConsumerThenUses() { 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); diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java index 7353318fb90..20d18018578 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java @@ -53,7 +53,10 @@ public void resolveWhenCustomParametersConsumerThenUses() { Consumer 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, diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java index 6f5d9e48d06..39f4b162fc0 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java @@ -19,28 +19,25 @@ 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() { } public static String samlEncode(byte[] b) { - return BASE64.encodeAsString(b); + return Base64.getEncoder().encodeToString(b); } public static byte[] samlDecode(String s) { - return BASE64.decode(s); + return Base64.getMimeDecoder().decode(s); } public static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java index 55cd6b53b9c..54b681842e9 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java @@ -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 static Saml2X509Credential relyingPartyVerifyingCredential() { 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 static Saml2X509Credential relyingPartyDecryptingCredential() { 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 @@ private static PrivateKey spPrivateKey() { + "-----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-----"); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java new file mode 100644 index 00000000000..d2da7dde20b --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultSaml2AuthenticatedPrincipalMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + this.mapper = new ObjectMapper(); + ClassLoader loader = getClass().getClassLoader(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + void shouldSerialize() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = TestSaml2JsonPayloads.createDefaultPrincipal(); + + String principalJson = this.mapper.writeValueAsString(principal); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON, principalJson, true); + } + + @Test + void shouldSerializeWithoutRegistrationId() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal( + TestSaml2JsonPayloads.PRINCIPAL_NAME, TestSaml2JsonPayloads.ATTRIBUTES, + TestSaml2JsonPayloads.SESSION_INDEXES); + + String principalJson = this.mapper.writeValueAsString(principal); + + JSONAssert.assertEquals(principalWithoutRegId(), principalJson, true); + } + + @Test + void shouldSerializeWithoutIndices() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal( + TestSaml2JsonPayloads.PRINCIPAL_NAME, TestSaml2JsonPayloads.ATTRIBUTES); + principal.setRelyingPartyRegistrationId(TestSaml2JsonPayloads.REG_ID); + + String principalJson = this.mapper.writeValueAsString(principal); + + JSONAssert.assertEquals(principalWithoutIndices(), principalJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = this.mapper.readValue( + TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON, DefaultSaml2AuthenticatedPrincipal.class); + + assertThat(principal).isNotNull(); + assertThat(principal.getName()).isEqualTo(TestSaml2JsonPayloads.PRINCIPAL_NAME); + assertThat(principal.getRelyingPartyRegistrationId()).isEqualTo(TestSaml2JsonPayloads.REG_ID); + assertThat(principal.getAttributes()).isEqualTo(TestSaml2JsonPayloads.ATTRIBUTES); + assertThat(principal.getSessionIndexes()).isEqualTo(TestSaml2JsonPayloads.SESSION_INDEXES); + } + + @Test + void shouldDeserializeWithoutRegistrationId() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = this.mapper.readValue(principalWithoutRegId(), + DefaultSaml2AuthenticatedPrincipal.class); + + assertThat(principal).isNotNull(); + assertThat(principal.getName()).isEqualTo(TestSaml2JsonPayloads.PRINCIPAL_NAME); + assertThat(principal.getRelyingPartyRegistrationId()).isNull(); + assertThat(principal.getAttributes()).isEqualTo(TestSaml2JsonPayloads.ATTRIBUTES); + assertThat(principal.getSessionIndexes()).isEqualTo(TestSaml2JsonPayloads.SESSION_INDEXES); + } + + private static String principalWithoutRegId() { + return TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON.replace(TestSaml2JsonPayloads.REG_ID_JSON, + "null"); + } + + private static String principalWithoutIndices() { + return TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON + .replace(TestSaml2JsonPayloads.SESSION_INDEXES_JSON, "[\"java.util.Collections$EmptyList\", []]"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationExceptionMixinTest.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationExceptionMixinTest.java new file mode 100644 index 00000000000..b6c2b22711c --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationExceptionMixinTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2AuthenticationExceptionMixinTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + this.mapper = new ObjectMapper(); + ClassLoader loader = getClass().getClassLoader(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + void shouldSerialize() throws Exception { + Saml2AuthenticationException exception = TestSaml2JsonPayloads.createDefaultSaml2AuthenticationException(); + + String exceptionJson = this.mapper.writeValueAsString(exception); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_SAML_AUTH_EXCEPTION_JSON, exceptionJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2AuthenticationException exception = this.mapper + .readValue(TestSaml2JsonPayloads.DEFAULT_SAML_AUTH_EXCEPTION_JSON, Saml2AuthenticationException.class); + + assertThat(exception).isNotNull(); + assertThat(exception.getMessage()).isEqualTo("exceptionMessage"); + assertThat(exception.getSaml2Error()).extracting(Saml2Error::getErrorCode, Saml2Error::getDescription) + .contains("errorCode", "errorDescription"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java new file mode 100644 index 00000000000..37b38ce1028 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2AuthenticationMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + this.mapper = new ObjectMapper(); + ClassLoader loader = getClass().getClassLoader(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + void shouldSerialize() throws Exception { + Saml2Authentication authentication = TestSaml2JsonPayloads.createDefaultAuthentication(); + + String authenticationJson = this.mapper.writeValueAsString(authentication); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_SAML2AUTHENTICATION_JSON, authenticationJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2Authentication authentication = this.mapper + .readValue(TestSaml2JsonPayloads.DEFAULT_SAML2AUTHENTICATION_JSON, Saml2Authentication.class); + + assertThat(authentication).isNotNull(); + assertThat(authentication.getDetails()).isEqualTo(TestSaml2JsonPayloads.DETAILS); + assertThat(authentication.getCredentials()).isEqualTo(TestSaml2JsonPayloads.SAML_RESPONSE); + assertThat(authentication.getSaml2Response()).isEqualTo(TestSaml2JsonPayloads.SAML_RESPONSE); + assertThat(authentication.getAuthorities()).isEqualTo(TestSaml2JsonPayloads.AUTHORITIES); + assertThat(authentication.getPrincipal()).usingRecursiveComparison() + .isEqualTo(TestSaml2JsonPayloads.createDefaultPrincipal()); + assertThat(authentication.getDetails()).usingRecursiveComparison().isEqualTo(TestSaml2JsonPayloads.DETAILS); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java new file mode 100644 index 00000000000..965b82257bd --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2LogoutRequestMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + this.mapper = new ObjectMapper(); + ClassLoader loader = getClass().getClassLoader(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + void shouldSerialize() throws Exception { + Saml2LogoutRequest request = TestSaml2JsonPayloads.createDefaultSaml2LogoutRequest(); + + String requestJson = this.mapper.writeValueAsString(request); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_LOGOUT_REQUEST_JSON, requestJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2LogoutRequest logoutRequest = this.mapper.readValue(TestSaml2JsonPayloads.DEFAULT_LOGOUT_REQUEST_JSON, + Saml2LogoutRequest.class); + + assertThat(logoutRequest).isNotNull(); + assertThat(logoutRequest.getId()).isEqualTo(TestSaml2JsonPayloads.ID); + assertThat(logoutRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + assertThat(logoutRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(logoutRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(logoutRequest.getLocation()).isEqualTo(TestSaml2JsonPayloads.LOCATION); + assertThat(logoutRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + Map expectedParams = new HashMap<>(); + expectedParams.put("SAMLRequest", TestSaml2JsonPayloads.SAML_REQUEST); + expectedParams.put("RelayState", TestSaml2JsonPayloads.RELAY_STATE); + expectedParams.put("AdditionalParam", TestSaml2JsonPayloads.ADDITIONAL_PARAM); + assertThat(logoutRequest.getParameters()).containsAllEntriesOf(expectedParams); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java new file mode 100644 index 00000000000..3183d274349 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2PostAuthenticationRequestMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + this.mapper = new ObjectMapper(); + ClassLoader loader = getClass().getClassLoader(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + void shouldSerialize() throws Exception { + Saml2PostAuthenticationRequest request = TestSaml2JsonPayloads.createDefaultSaml2PostAuthenticationRequest(); + + String requestJson = this.mapper.writeValueAsString(request); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON, requestJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2PostAuthenticationRequest authRequest = this.mapper + .readValue(TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON, Saml2PostAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + } + + @Test + void shouldDeserializeWithNoRegistrationId() throws Exception { + String json = TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON.replace( + "\"relyingPartyRegistrationId\": \"" + TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID + "\",", ""); + + Saml2PostAuthenticationRequest authRequest = this.mapper.readValue(json, Saml2PostAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getRelyingPartyRegistrationId()).isNull(); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java new file mode 100644 index 00000000000..d9cfa77fad4 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2RedirectAuthenticationRequestMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + this.mapper = new ObjectMapper(); + ClassLoader loader = getClass().getClassLoader(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + void shouldSerialize() throws Exception { + Saml2RedirectAuthenticationRequest request = TestSaml2JsonPayloads + .createDefaultSaml2RedirectAuthenticationRequest(); + + String requestJson = this.mapper.writeValueAsString(request); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON, requestJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2RedirectAuthenticationRequest authRequest = this.mapper.readValue( + TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON, Saml2RedirectAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getSigAlg()).isEqualTo(TestSaml2JsonPayloads.SIG_ALG); + assertThat(authRequest.getSignature()).isEqualTo(TestSaml2JsonPayloads.SIGNATURE); + assertThat(authRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + } + + @Test + void shouldDeserializeWithNoRegistrationId() throws Exception { + String json = TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON.replace( + "\"relyingPartyRegistrationId\": \"" + TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID + "\",", ""); + + Saml2RedirectAuthenticationRequest authRequest = this.mapper.readValue(json, + Saml2RedirectAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getSigAlg()).isEqualTo(TestSaml2JsonPayloads.SIG_ALG); + assertThat(authRequest.getSignature()).isEqualTo(TestSaml2JsonPayloads.SIGNATURE); + assertThat(authRequest.getRelyingPartyRegistrationId()).isNull(); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java new file mode 100644 index 00000000000..18f2e7deb8a --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson2; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +final class TestSaml2JsonPayloads { + + private TestSaml2JsonPayloads() { + } + + static final Map> ATTRIBUTES; + + static { + Map> tmpAttributes = new HashMap<>(); + tmpAttributes.put("name", Collections.singletonList("attr_name")); + tmpAttributes.put("email", Collections.singletonList("attr_email")); + tmpAttributes.put("listOf", Collections.unmodifiableList(Arrays.asList("Element1", "Element2", 4, true))); + ATTRIBUTES = Collections.unmodifiableMap(tmpAttributes); + } + + static final String REG_ID = "REG_ID_TEST"; + static final String REG_ID_JSON = "\"" + REG_ID + "\""; + + static final String SESSION_INDEXES_JSON = "[" + " \"java.util.Collections$UnmodifiableRandomAccessList\"," + + " [ \"Index 1\", \"Index 2\" ]" + "]"; + static final List SESSION_INDEXES = Collections.unmodifiableList(Arrays.asList("Index 1", "Index 2")); + + static final String PRINCIPAL_NAME = "principalName"; + + // @formatter:off + static final String DEFAULT_AUTHENTICATED_PRINCIPAL_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal\"," + + " \"name\": \"" + PRINCIPAL_NAME + "\"," + + " \"attributes\": {" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\"," + + " \"listOf\": [" + + " \"java.util.Collections$UnmodifiableRandomAccessList\"," + + " [ \"Element1\", \"Element2\", 4, true ]" + + " ]," + + " \"email\": [" + + " \"java.util.Collections$SingletonList\"," + + " [ \"attr_email\" ]" + + " ]," + + " \"name\": [" + + " \"java.util.Collections$SingletonList\"," + + " [ \"attr_name\" ]" + + " ]" + + " }," + + " \"sessionIndexes\": " + SESSION_INDEXES_JSON + "," + + " \"registrationId\": " + REG_ID_JSON + "" + + "}"; + // @formatter:on + + static DefaultSaml2AuthenticatedPrincipal createDefaultPrincipal() { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(PRINCIPAL_NAME, + ATTRIBUTES, SESSION_INDEXES); + principal.setRelyingPartyRegistrationId(REG_ID); + return principal; + } + + static final String SAML_REQUEST = "samlRequestValue"; + static final String RELAY_STATE = "relayStateValue"; + static final String AUTHENTICATION_REQUEST_URI = "authenticationRequestUriValue"; + static final String RELYINGPARTY_REGISTRATION_ID = "registrationIdValue"; + static final String SIG_ALG = "sigAlgValue"; + static final String SIGNATURE = "signatureValue"; + + // @formatter:off + static final String DEFAULT_REDIRECT_AUTH_REQUEST_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest\"," + + " \"samlRequest\": \"" + SAML_REQUEST + "\"," + + " \"relayState\": \"" + RELAY_STATE + "\"," + + " \"authenticationRequestUri\": \"" + AUTHENTICATION_REQUEST_URI + "\"," + + " \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\"," + + " \"sigAlg\": \"" + SIG_ALG + "\"," + + " \"signature\": \"" + SIGNATURE + "\"" + + "}"; + // @formatter:on + + // @formatter:off + static final String DEFAULT_POST_AUTH_REQUEST_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest\"," + + " \"samlRequest\": \"" + SAML_REQUEST + "\"," + + " \"relayState\": \"" + RELAY_STATE + "\"," + + " \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\"," + + " \"authenticationRequestUri\": \"" + AUTHENTICATION_REQUEST_URI + "\"" + + "}"; + // @formatter:on + + static final String ID = "idValue"; + static final String LOCATION = "locationValue"; + static final String BINDNG = "REDIRECT"; + static final String ADDITIONAL_PARAM = "additionalParamValue"; + + // @formatter:off + static final String DEFAULT_LOGOUT_REQUEST_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest\"," + + " \"id\": \"" + ID + "\"," + + " \"location\": \"" + LOCATION + "\"," + + " \"binding\": \"" + BINDNG + "\"," + + " \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\"," + + " \"parameters\": { " + + " \"@class\": \"java.util.Collections$UnmodifiableMap\"," + + " \"SAMLRequest\": \"" + SAML_REQUEST + "\"," + + " \"RelayState\": \"" + RELAY_STATE + "\"," + + " \"AdditionalParam\": \"" + ADDITIONAL_PARAM + "\"" + + " }" + + "}"; + // @formatter:on + + static Saml2PostAuthenticationRequest createDefaultSaml2PostAuthenticationRequest() { + return Saml2PostAuthenticationRequest.withRelyingPartyRegistration( + TestRelyingPartyRegistrations.full().registrationId(RELYINGPARTY_REGISTRATION_ID) + .assertingPartyDetails((party) -> party.singleSignOnServiceLocation(AUTHENTICATION_REQUEST_URI)) + .build()) + .samlRequest(SAML_REQUEST).relayState(RELAY_STATE).build(); + } + + static Saml2RedirectAuthenticationRequest createDefaultSaml2RedirectAuthenticationRequest() { + return Saml2RedirectAuthenticationRequest + .withRelyingPartyRegistration(TestRelyingPartyRegistrations.full() + .registrationId(RELYINGPARTY_REGISTRATION_ID) + .assertingPartyDetails((party) -> party.singleSignOnServiceLocation(AUTHENTICATION_REQUEST_URI)) + .build()) + .samlRequest(SAML_REQUEST).relayState(RELAY_STATE).sigAlg(SIG_ALG).signature(SIGNATURE).build(); + } + + static Saml2LogoutRequest createDefaultSaml2LogoutRequest() { + return Saml2LogoutRequest + .withRelyingPartyRegistration( + TestRelyingPartyRegistrations.full().registrationId(RELYINGPARTY_REGISTRATION_ID) + .assertingPartyDetails((party) -> party.singleLogoutServiceLocation(LOCATION) + .singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT)) + .build()) + .id(ID).samlRequest(SAML_REQUEST).relayState(RELAY_STATE) + .parameters((params) -> params.put("AdditionalParam", ADDITIONAL_PARAM)).build(); + } + + static final Collection AUTHORITIES = Collections + .unmodifiableList(Arrays.asList(new SimpleGrantedAuthority("Role1"), new SimpleGrantedAuthority("Role2"))); + + static final Object DETAILS = User.withUsername("username").password("empty").authorities("A", "B").build(); + static final String SAML_RESPONSE = "samlResponseValue"; + + // @formatter:off + static final String DEFAULT_SAML2AUTHENTICATION_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2Authentication\"," + + " \"authorities\": [" + + " \"java.util.Collections$UnmodifiableRandomAccessList\"," + + " [" + + " {" + + " \"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\": \"Role1\"" + + " }," + + " {" + + " \"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\": \"Role2\"" + + " }" + + " ]" + + " ]," + + " \"details\": {" + + " \"@class\": \"org.springframework.security.core.userdetails.User\"," + + " \"password\": \"empty\"," + + " \"username\": \"username\"," + + " \"authorities\": [" + + " \"java.util.Collections$UnmodifiableSet\", [" + + " {" + + " \"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\":\"A\"" + + " }," + + " {" + + " \"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\":\"B\"" + + " }" + + " ]]," + + " \"accountNonExpired\": true," + + " \"accountNonLocked\": true," + + " \"credentialsNonExpired\": true," + + " \"enabled\": true" + + " }," + + " \"principal\": " + DEFAULT_AUTHENTICATED_PRINCIPAL_JSON + "," + + " \"saml2Response\": \"" + SAML_RESPONSE + "\"" + + "}"; + // @formatter:on + + static Saml2Authentication createDefaultAuthentication() { + DefaultSaml2AuthenticatedPrincipal principal = createDefaultPrincipal(); + Saml2Authentication authentication = new Saml2Authentication(principal, SAML_RESPONSE, AUTHORITIES); + authentication.setDetails(DETAILS); + return authentication; + } + + // @formatter:off + static final String DEFAULT_SAML_AUTH_EXCEPTION_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException\"," + + " \"detailMessage\": \"exceptionMessage\"," + + " \"error\": {" + + " \"@class\": \"org.springframework.security.saml2.core.Saml2Error\"," + + " \"errorCode\": \"errorCode\"," + + " \"description\": \"errorDescription\"" + + " }" + + "}"; + // @formatter:on + + static Saml2AuthenticationException createDefaultSaml2AuthenticationException() { + return new Saml2AuthenticationException(new Saml2Error("errorCode", "errorDescription"), "exceptionMessage"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java new file mode 100644 index 00000000000..748bfcdc665 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.SerializationUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2PostAuthenticationRequestTests { + + private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; + + @Test + void serializeWhenDeserializeThenSameFields() { + Saml2PostAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + byte[] bytes = SerializationUtils.serialize(authenticationRequest); + Saml2PostAuthenticationRequest deserializedAuthenticationRequest = (Saml2PostAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isEqualTo(authenticationRequest); + } + + @Test + void serializeWhenDeserializeAndCompareToOtherThenNotSame() { + Saml2PostAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + Saml2PostAuthenticationRequest otherAuthenticationRequest = getAuthenticationRequestBuilder() + .relayState("relay").build(); + byte[] bytes = SerializationUtils.serialize(otherAuthenticationRequest); + Saml2PostAuthenticationRequest deserializedAuthenticationRequest = (Saml2PostAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isNotEqualTo(authenticationRequest); + } + + private Saml2PostAuthenticationRequest.Builder getAuthenticationRequestBuilder() { + return Saml2PostAuthenticationRequest + .withAuthenticationRequestContext( + TestSaml2AuthenticationRequestContexts.authenticationRequestContext().build()) + .samlRequest("request").authenticationRequestUri(IDP_SSO_URL); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java new file mode 100644 index 00000000000..e2878455d88 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.SerializationUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2RedirectAuthenticationRequestTests { + + private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; + + @Test + void serializeWhenDeserializeThenSameFields() { + Saml2RedirectAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + byte[] bytes = SerializationUtils.serialize(authenticationRequest); + Saml2RedirectAuthenticationRequest deserializedAuthenticationRequest = (Saml2RedirectAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isEqualTo(authenticationRequest); + } + + @Test + void serializeWhenDeserializeAndCompareToOtherThenNotSame() { + Saml2RedirectAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + Saml2RedirectAuthenticationRequest otherAuthenticationRequest = getAuthenticationRequestBuilder() + .relayState("relay").build(); + byte[] bytes = SerializationUtils.serialize(otherAuthenticationRequest); + Saml2RedirectAuthenticationRequest deserializedAuthenticationRequest = (Saml2RedirectAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isNotEqualTo(authenticationRequest); + } + + private Saml2RedirectAuthenticationRequest.Builder getAuthenticationRequestBuilder() { + return Saml2RedirectAuthenticationRequest + .withAuthenticationRequestContext( + TestSaml2AuthenticationRequestContexts.authenticationRequestContext().build()) + .samlRequest("request").authenticationRequestUri(IDP_SSO_URL); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestCustomOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestCustomOpenSamlObjects.java new file mode 100644 index 00000000000..5da096fea88 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestCustomOpenSamlObjects.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.xml.namespace.QName; + +import net.shibboleth.utilities.java.support.xml.ElementSupport; +import org.opensaml.core.xml.AbstractXMLObject; +import org.opensaml.core.xml.AbstractXMLObjectBuilder; +import org.opensaml.core.xml.ElementExtensibleXMLObject; +import org.opensaml.core.xml.Namespace; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.AbstractXMLObjectMarshaller; +import org.opensaml.core.xml.io.AbstractXMLObjectUnmarshaller; +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.core.xml.schema.XSAny; +import org.opensaml.core.xml.schema.impl.XSAnyBuilder; +import org.opensaml.core.xml.util.IndexedXMLObjectChildrenList; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.core.AttributeValue; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; + +public final class TestCustomOpenSamlObjects { + + static { + OpenSamlInitializationService.initialize(); + XMLObjectProviderRegistrySupport.getMarshallerFactory().registerMarshaller(CustomOpenSamlObject.TYPE_NAME, + new TestCustomOpenSamlObjects.CustomSamlObjectMarshaller()); + XMLObjectProviderRegistrySupport.getUnmarshallerFactory().registerUnmarshaller(CustomOpenSamlObject.TYPE_NAME, + new TestCustomOpenSamlObjects.CustomSamlObjectUnmarshaller()); + } + + public static CustomOpenSamlObject instance() { + CustomOpenSamlObject samlObject = new TestCustomOpenSamlObjects.CustomSamlObjectBuilder() + .buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, CustomOpenSamlObject.TYPE_NAME); + XSAny street = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "Street", + CustomOpenSamlObject.TYPE_CUSTOM_PREFIX); + street.setTextContent("Test Street"); + samlObject.getUnknownXMLObjects().add(street); + XSAny streetNumber = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "Number", + CustomOpenSamlObject.TYPE_CUSTOM_PREFIX); + streetNumber.setTextContent("1"); + samlObject.getUnknownXMLObjects().add(streetNumber); + XSAny zip = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "ZIP", + CustomOpenSamlObject.TYPE_CUSTOM_PREFIX); + zip.setTextContent("11111"); + samlObject.getUnknownXMLObjects().add(zip); + XSAny city = new XSAnyBuilder().buildObject(CustomOpenSamlObject.CUSTOM_NS, "City", + CustomOpenSamlObject.TYPE_CUSTOM_PREFIX); + city.setTextContent("Test City"); + samlObject.getUnknownXMLObjects().add(city); + return samlObject; + } + + private TestCustomOpenSamlObjects() { + + } + + public interface CustomOpenSamlObject extends ElementExtensibleXMLObject { + + String TYPE_LOCAL_NAME = "CustomType"; + + String TYPE_CUSTOM_PREFIX = "custom"; + + String CUSTOM_NS = "https://custom.com/schema/custom"; + + /** QName of the CustomType type. */ + QName TYPE_NAME = new QName(CUSTOM_NS, TYPE_LOCAL_NAME, TYPE_CUSTOM_PREFIX); + + String getStreet(); + + String getStreetNumber(); + + String getZIP(); + + String getCity(); + + } + + public static class CustomOpenSamlObjectImpl extends AbstractXMLObject implements CustomOpenSamlObject { + + @Nonnull + private IndexedXMLObjectChildrenList unknownXMLObjects; + + /** + * Constructor. + * @param namespaceURI the namespace the element is in + * @param elementLocalName the local name of the XML element this Object + * represents + * @param namespacePrefix the prefix for the given namespace + */ + protected CustomOpenSamlObjectImpl(@Nullable String namespaceURI, @Nonnull String elementLocalName, + @Nullable String namespacePrefix) { + super(namespaceURI, elementLocalName, namespacePrefix); + super.getNamespaceManager().registerNamespaceDeclaration(new Namespace(CUSTOM_NS, TYPE_CUSTOM_PREFIX)); + this.unknownXMLObjects = new IndexedXMLObjectChildrenList<>(this); + } + + @Nonnull + @Override + public List getUnknownXMLObjects() { + return this.unknownXMLObjects; + } + + @Nonnull + @Override + public List getUnknownXMLObjects(@Nonnull QName typeOrName) { + return (List) this.unknownXMLObjects.subList(typeOrName); + } + + @Nullable + @Override + public List getOrderedChildren() { + return Collections.unmodifiableList(this.unknownXMLObjects); + } + + @Override + public String getStreet() { + return ((XSAny) getOrderedChildren().get(0)).getTextContent(); + } + + @Override + public String getStreetNumber() { + return ((XSAny) getOrderedChildren().get(1)).getTextContent(); + } + + @Override + public String getZIP() { + return ((XSAny) getOrderedChildren().get(2)).getTextContent(); + } + + @Override + public String getCity() { + return ((XSAny) getOrderedChildren().get(3)).getTextContent(); + } + + } + + public static class CustomSamlObjectBuilder extends AbstractXMLObjectBuilder { + + @Nonnull + @Override + public CustomOpenSamlObject buildObject(@Nullable String namespaceURI, @Nonnull String localName, + @Nullable String namespacePrefix) { + return new CustomOpenSamlObjectImpl(namespaceURI, localName, namespacePrefix); + } + + } + + public static class CustomSamlObjectMarshaller extends AbstractXMLObjectMarshaller { + + public CustomSamlObjectMarshaller() { + super(); + } + + @Override + protected void marshallElementContent(@Nonnull XMLObject xmlObject, @Nonnull Element domElement) { + final CustomOpenSamlObject customSamlObject = (CustomOpenSamlObject) xmlObject; + + for (XMLObject object : customSamlObject.getOrderedChildren()) { + ElementSupport.appendChildElement(domElement, object.getDOM()); + } + } + + } + + public static class CustomSamlObjectUnmarshaller extends AbstractXMLObjectUnmarshaller { + + public CustomSamlObjectUnmarshaller() { + super(); + } + + @Override + protected void processChildElement(@Nonnull XMLObject parentXMLObject, @Nonnull XMLObject childXMLObject) + throws UnmarshallingException { + final CustomOpenSamlObject customSamlObject = (CustomOpenSamlObject) parentXMLObject; + super.processChildElement(customSamlObject, childXMLObject); + customSamlObject.getUnknownXMLObjects().add(childXMLObject); + } + + @Nonnull + @Override + protected XMLObject buildXMLObject(@Nonnull Element domElement) { + return new CustomOpenSamlObjectImpl(SAMLConstants.SAML20_NS, AttributeValue.DEFAULT_ELEMENT_LOCAL_NAME, + CustomOpenSamlObject.TYPE_CUSTOM_PREFIX); + } + + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 0f38823e224..e0edad71366 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -49,6 +49,7 @@ import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeValue; import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; @@ -153,6 +154,9 @@ static Assertion assertion(String username, String issuerEntityId, String recipi confirmationData.setRecipient(recipientUri); subjectConfirmation.setSubjectConfirmationData(confirmationData); assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); + AuthnStatement statement = build(AuthnStatement.DEFAULT_ELEMENT_NAME); + statement.setSessionIndex("session-index"); + assertion.getAuthnStatements().add(statement); return assertion; } @@ -292,6 +296,17 @@ static Attribute attribute(String name, String value) { return attribute; } + static AttributeStatement customAttributeStatement(String attributeName, XMLObject customAttributeValue) { + AttributeStatementBuilder attributeStatementBuilder = new AttributeStatementBuilder(); + AttributeBuilder attributeBuilder = new AttributeBuilder(); + Attribute attribute = attributeBuilder.buildObject(); + attribute.setName(attributeName); + attribute.getAttributeValues().add(customAttributeValue); + AttributeStatement attributeStatement = attributeStatementBuilder.buildObject(); + attributeStatement.getAttributes().add(attribute); + return attributeStatement; + } + static List attributeStatements() { List attributeStatements = new ArrayList<>(); AttributeStatementBuilder attributeStatementBuilder = new AttributeStatementBuilder(); @@ -365,6 +380,26 @@ public static LogoutRequest assertingPartyLogoutRequest(RelyingPartyRegistration return logoutRequest; } + public static LogoutRequest assertingPartyLogoutRequestNameIdInEncryptedId(RelyingPartyRegistration registration) { + LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder(); + LogoutRequest logoutRequest = logoutRequestBuilder.buildObject(); + logoutRequest.setID("id"); + NameIDBuilder nameIdBuilder = new NameIDBuilder(); + NameID nameId = nameIdBuilder.buildObject(); + nameId.setValue("user"); + logoutRequest.setNameID(null); + Saml2X509Credential credential = registration.getAssertingPartyDetails().getEncryptionX509Credentials() + .iterator().next(); + EncryptedID encrypted = encrypted(nameId, credential); + logoutRequest.setEncryptedID(encrypted); + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(registration.getAssertingPartyDetails().getEntityId()); + logoutRequest.setIssuer(issuer); + logoutRequest.setDestination(registration.getSingleLogoutServiceLocation()); + return logoutRequest; + } + public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) { LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder(); LogoutResponse logoutResponse = logoutResponseBuilder.buildObject(); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java index e5c826fe371..8d1ef354bfc 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java @@ -60,6 +60,20 @@ public void handleWhenPostBindingThenValidates() { assertThat(result.hasErrors()).isFalse(); } + @Test + public void handleWhenNameIdIsEncryptedIdPostThenValidates() { + + RelyingPartyRegistration registration = decrypting(encrypting(registration())).build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequestNameIdInEncryptedId(registration); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.manager.validate(parameters); + assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse(); + + } + @Test public void handleWhenRedirectBindingThenValidatesSignatureParameter() { RelyingPartyRegistration registration = registration() @@ -129,11 +143,38 @@ public void handleWhenMismatchedDestinationThenInvalidDestinationError() { 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)); } + private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) { + return builder + .decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential())); + } + + private RelyingPartyRegistration.Builder encrypting(RelyingPartyRegistration.Builder builder) { + return builder.assertingPartyDetails((party) -> party.encryptionX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential()))); + } + private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) { return builder.assertingPartyDetails((party) -> party .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java index a43f47a3468..2081a52a6c9 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java @@ -119,6 +119,24 @@ public void handleWhenStatusNotSuccessThenInvalidResponseError() { 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)); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java index d42fc875be6..0d75992cd81 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java @@ -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. @@ -36,9 +36,8 @@ public void resolveWhenRelyingPartyThenMetadataMatches() { .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT).build(); OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); - assertThat(metadata).contains("") - .contains("") + assertThat(metadata).contains("").contains("") .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") @@ -53,12 +52,41 @@ public void resolveWhenRelyingPartyNoCredentialsThenMetadataMatches() { .build(); OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); - assertThat(metadata).contains("") + assertThat(metadata).contains("") .doesNotContain("") .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"") .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); } + @Test + public void resolveWhenRelyingPartyNameIDFormatThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full().nameIdFormat("format") + .build(); + OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); + String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("format"); + } + + @Test + public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .singleLogoutServiceLocation(null).nameIdFormat("format").build(); + OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); + String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).doesNotContain("ResponseLocation"); + } + + @Test + public void resolveWhenEntityDescriptorCustomizerThenUses() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .entityId("originalEntityId").build(); + OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); + openSamlMetadataResolver.setEntityDescriptorCustomizer( + (parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId")); + String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains(""; - private OpenSamlAssertingPartyMetadataConverter converter; + private OpenSamlMetadataAssertingPartyDetailsConverter converter; @BeforeEach public void setup() { - this.converter = new OpenSamlAssertingPartyMetadataConverter(); + this.converter = new OpenSamlMetadataAssertingPartyDetailsConverter(); } @Test @@ -98,8 +99,8 @@ public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception { + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build(); - RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next() + .build(); assertThat(details.getWantAuthnRequestsSigned()).isFalse(); assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512); assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); @@ -111,6 +112,11 @@ public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception { assertThat(details.getEncryptionX509Credentials()).hasSize(1); assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) .isEqualTo(x509Certificate(CERTIFICATE)); + assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class); + OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details; + EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor(); + assertThat(entityDescriptor).isNotNull(); + assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId()); } // gh-9051 @@ -123,8 +129,8 @@ public void readWhenEntitiesDescriptorThenConfigures() throws Exception { + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)))); InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build(); - RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next() + .build(); assertThat(details.getWantAuthnRequestsSigned()).isFalse(); assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); @@ -142,8 +148,8 @@ public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exc String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build(); - RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next() + .build(); assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) .isEqualTo(x509Certificate(CERTIFICATE)); assertThat(details.getEncryptionX509Credentials()).hasSize(1); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java index d25d4b981c0..eaefe22f822 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java @@ -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 @@ 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; @@ -28,6 +29,7 @@ public class RelyingPartyRegistrationTests { @Test public void withRelyingPartyRegistrationWorks() { RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .nameIdFormat("format") .assertingPartyDetails((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) .assertingPartyDetails((a) -> a.wantAuthnRequestsSigned(false)) .assertingPartyDetails((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) @@ -74,6 +76,7 @@ private void compareRegistrations(RelyingPartyRegistration registration, Relying .isEqualTo(registration.getAssertingPartyDetails().getVerificationX509Credentials()); assertThat(copy.getAssertingPartyDetails().getSigningAlgorithms()) .isEqualTo(registration.getAssertingPartyDetails().getSigningAlgorithms()); + assertThat(copy.getNameIdFormat()).isEqualTo(registration.getNameIdFormat()); } @Test @@ -81,9 +84,68 @@ public void buildWhenUsingDefaultsThenAssertionConsumerServiceBindingDefaultsToP 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); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java index 0aadfbb5325..9360475d68a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -21,6 +21,7 @@ import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.List; import java.util.stream.Collectors; import okhttp3.mockwebserver.MockResponse; @@ -41,12 +42,18 @@ public class RelyingPartyRegistrationsTests { private String metadata; + private String entitiesDescriptor; + @BeforeEach public void setup() throws Exception { ClassPathResource resource = new ClassPathResource("test-metadata.xml"); try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { this.metadata = reader.lines().collect(Collectors.joining()); } + resource = new ClassPathResource("test-entitiesdescriptor.xml"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + this.entitiesDescriptor = reader.lines().collect(Collectors.joining()); + } } @Test @@ -129,4 +136,111 @@ public void fromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exceptio } } + @Test + public void collectionFromMetadataLocationWhenResolvableThenPopulatesBuilder() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setBody(this.entitiesDescriptor).setResponseCode(200)); + List registrations = RelyingPartyRegistrations + .collectionFromMetadataLocation(server.url("/").toString()).stream() + .map((r) -> r.entityId("rp").build()).collect(Collectors.toList()); + assertThat(registrations).hasSize(2); + RelyingPartyRegistration first = registrations.get(0); + RelyingPartyRegistration.AssertingPartyDetails details = first.getAssertingPartyDetails(); + assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(details.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + RelyingPartyRegistration second = registrations.get(1); + details = second.getAssertingPartyDetails(); + assertThat(details.getEntityId()).isEqualTo("https://ap.example.org/idp/shibboleth"); + assertThat(details.getSingleSignOnServiceLocation()) + .isEqualTo("https://ap.example.org/idp/profile/SAML2/POST/SSO"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + } + } + + @Test + public void collectionFromMetadataLocationWhenUnresolvableThenSaml2Exception() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200)); + String url = server.url("/").toString(); + server.shutdown(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation(url)); + } + } + + @Test + public void collectionFromMetadataLocationWhenMalformedResponseThenSaml2Exception() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setBody("malformed").setResponseCode(200)); + String url = server.url("/").toString(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation(url)); + } + } + + @Test + public void collectionFromMetadataFileWhenResolvableThenPopulatesBuilder() { + File file = new File("src/test/resources/test-entitiesdescriptor.xml"); + RelyingPartyRegistration registration = RelyingPartyRegistrations + .collectionFromMetadataLocation("file:" + file.getAbsolutePath()).stream() + .map((r) -> r.entityId("rp").build()).findFirst().get(); + RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(details.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + } + + @Test + public void collectionFromMetadataFileWhenContainsOnlyEntityDescriptorThenPopulatesBuilder() { + File file = new File("src/test/resources/test-metadata.xml"); + RelyingPartyRegistration registration = RelyingPartyRegistrations + .collectionFromMetadataLocation("file:" + file.getAbsolutePath()).stream() + .map((r) -> r.entityId("rp").build()).findFirst().get(); + RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(details.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + } + + @Test + public void collectionFromMetadataFileWhenNotFoundThenSaml2Exception() { + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation("filePath")); + } + + @Test + public void collectionFromMetadataInputStreamWhenResolvableThenPopulatesBuilder() throws Exception { + try (InputStream source = new ByteArrayInputStream(this.entitiesDescriptor.getBytes())) { + RelyingPartyRegistration registration = RelyingPartyRegistrations.collectionFromMetadata(source).stream() + .map((r) -> r.entityId("rp").build()).findFirst().get(); + RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(details.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + } + } + + @Test + public void collectionFromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exception { + try (InputStream source = new ByteArrayInputStream("".getBytes())) { + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadata(source)); + } + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java index 5ddd7168ed6..b9867e19084 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -20,6 +20,9 @@ import java.nio.charset.StandardCharsets; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,6 +46,7 @@ import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.util.HtmlUtils; @@ -69,6 +73,9 @@ public class Saml2WebSsoAuthenticationRequestFilterTests { private Saml2AuthenticationRequestContextResolver resolver = mock(Saml2AuthenticationRequestContextResolver.class); + private Saml2AuthenticationRequestResolver authenticationRequestResolver = mock( + Saml2AuthenticationRequestResolver.class); + private Saml2AuthenticationRequestRepository authenticationRequestRepository = mock( Saml2AuthenticationRequestRepository.class); @@ -86,7 +93,12 @@ public void setup() { this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); this.request.setPathInfo("/saml2/authenticate/registration-id"); - this.filterChain = new MockFilterChain(); + this.filterChain = new MockFilterChain() { + @Override + public void doFilter(ServletRequest request, ServletResponse response) { + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + }; this.rpBuilder = RelyingPartyRegistration.withRegistrationId("registration-id") .providerDetails((c) -> c.entityId("idp-entity-id")).providerDetails((c) -> c.webSsoUrl(IDP_SSO_URL)) .assertionConsumerServiceUrlTemplate("template") @@ -114,6 +126,12 @@ private static Saml2RedirectAuthenticationRequest.Builder redirectAuthentication .authenticationRequestUri(IDP_SSO_URL); } + private static Saml2RedirectAuthenticationRequest.Builder redirectAuthenticationRequest( + RelyingPartyRegistration registration) { + return Saml2RedirectAuthenticationRequest.withRelyingPartyRegistration(registration).samlRequest("request") + .authenticationRequestUri(IDP_SSO_URL); + } + private static Saml2PostAuthenticationRequest.Builder postAuthenticationRequest( Saml2AuthenticationRequestContext context) { return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context).samlRequest("request") @@ -287,4 +305,15 @@ public void doFilterWhenPathStartsWithRegistrationIdThenPosts() throws Exception verify(this.repository).findByRegistrationId("registration-id"); } + @Test + public void doFilterWhenCustomAuthenticationRequestResolverThenUses() throws Exception { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + Saml2RedirectAuthenticationRequest authenticationRequest = redirectAuthenticationRequest(registration).build(); + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter( + this.authenticationRequestResolver); + given(this.authenticationRequestResolver.resolve(any())).willReturn(authenticationRequest); + filter.doFilterInternal(this.request, this.response, this.filterChain); + verify(this.authenticationRequestResolver).resolve(any()); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java index 02b46929618..124e21f3d3a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java @@ -44,8 +44,11 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) public class Saml2AuthenticationTokenConverterTests { @@ -71,6 +74,21 @@ public void convertWhenSamlResponseThenToken() { .isEqualTo(this.relyingPartyRegistration.getRegistrationId()); } + @Test + public void convertWhenSamlResponseWithRelyingPartyRegistrationResolver( + @Mock RelyingPartyRegistrationResolver resolver) { + Saml2AuthenticationTokenConverter converter = new Saml2AuthenticationTokenConverter(resolver); + given(resolver.resolve(any(HttpServletRequest.class), any())).willReturn(this.relyingPartyRegistration); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter(Saml2ParameterNames.SAML_RESPONSE, + Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8))); + Saml2AuthenticationToken token = converter.convert(request); + assertThat(token.getSaml2Response()).isEqualTo("response"); + assertThat(token.getRelyingPartyRegistration().getRegistrationId()) + .isEqualTo(this.relyingPartyRegistration.getRegistrationId()); + verify(resolver).resolve(any(), isNull()); + } + @Test public void convertWhenSamlResponseInvalidBase64ThenSaml2AuthenticationException() { Saml2AuthenticationTokenConverter converter = new Saml2AuthenticationTokenConverter( @@ -159,6 +177,8 @@ public void convertWhenSavedAuthenticationRequestThenToken() { Saml2AuthenticationRequestRepository authenticationRequestRepository = mock( Saml2AuthenticationRequestRepository.class); AbstractSaml2AuthenticationRequest authenticationRequest = mock(AbstractSaml2AuthenticationRequest.class); + given(authenticationRequest.getRelyingPartyRegistrationId()) + .willReturn(this.relyingPartyRegistration.getRegistrationId()); Saml2AuthenticationTokenConverter converter = new Saml2AuthenticationTokenConverter( this.relyingPartyRegistrationResolver); converter.setAuthenticationRequestRepository(authenticationRequestRepository); @@ -176,6 +196,30 @@ public void convertWhenSavedAuthenticationRequestThenToken() { assertThat(token.getAuthenticationRequest()).isEqualTo(authenticationRequest); } + @Test + public void convertWhenSavedAuthenticationRequestThenTokenWithRelyingPartyRegistrationResolver( + @Mock RelyingPartyRegistrationResolver resolver) { + Saml2AuthenticationRequestRepository authenticationRequestRepository = mock( + Saml2AuthenticationRequestRepository.class); + AbstractSaml2AuthenticationRequest authenticationRequest = mock(AbstractSaml2AuthenticationRequest.class); + given(authenticationRequest.getRelyingPartyRegistrationId()) + .willReturn(this.relyingPartyRegistration.getRegistrationId()); + Saml2AuthenticationTokenConverter converter = new Saml2AuthenticationTokenConverter(resolver); + converter.setAuthenticationRequestRepository(authenticationRequestRepository); + given(resolver.resolve(any(HttpServletRequest.class), any())).willReturn(this.relyingPartyRegistration); + given(authenticationRequestRepository.loadAuthenticationRequest(any(HttpServletRequest.class))) + .willReturn(authenticationRequest); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter(Saml2ParameterNames.SAML_RESPONSE, + Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8))); + Saml2AuthenticationToken token = converter.convert(request); + assertThat(token.getSaml2Response()).isEqualTo("response"); + assertThat(token.getRelyingPartyRegistration().getRegistrationId()) + .isEqualTo(this.relyingPartyRegistration.getRegistrationId()); + assertThat(token.getAuthenticationRequest()).isEqualTo(authenticationRequest); + verify(resolver).resolve(any(), eq(this.relyingPartyRegistration.getRegistrationId())); + } + @Test public void constructorWhenResolverIsNullThenIllegalArgument() { assertThatIllegalArgumentException() diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java new file mode 100644 index 00000000000..b81e991b83c --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link OpenSamlAuthenticationRequestResolver} + */ +public class OpenSamlAuthenticationRequestResolverTests { + + private RelyingPartyRegistration.Builder relyingPartyRegistrationBuilder; + + @Before + public void setUp() { + this.relyingPartyRegistrationBuilder = TestRelyingPartyRegistrations.relyingPartyRegistration(); + } + + @Test + public void resolveAuthenticationRequestWhenSignedRedirectThenSignsAndRedirects() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setPathInfo("/saml2/authenticate/registration-id"); + RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build(); + OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); + Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { + assertThat(authnRequest.getAssertionConsumerServiceURL()) + .isEqualTo(registration.getAssertionConsumerServiceLocation()); + assertThat(authnRequest.getProtocolBinding()) + .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); + assertThat(authnRequest.getDestination()) + .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); + }); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isNotNull(); + assertThat(result.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + assertThat(result.getSignature()).isNotEmpty(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + } + + @Test + public void resolveAuthenticationRequestWhenUnsignedRedirectThenRedirectsAndNoSignature() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setPathInfo("/saml2/authenticate/registration-id"); + RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder + .assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build(); + OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); + Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { + assertThat(authnRequest.getAssertionConsumerServiceURL()) + .isEqualTo(registration.getAssertionConsumerServiceLocation()); + assertThat(authnRequest.getProtocolBinding()) + .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); + assertThat(authnRequest.getDestination()) + .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); + }); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isNotNull(); + assertThat(result.getSigAlg()).isNull(); + assertThat(result.getSignature()).isNull(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + } + + @Test + public void resolveAuthenticationRequestWhenSignedThenCredentialIsRequired() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setPathInfo("/saml2/authenticate/registration-id"); + Saml2X509Credential credential = TestSaml2X509Credentials.relyingPartyVerifyingCredential(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential))).build(); + OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> resolver.resolve(request, null)); + } + + @Test + public void resolveAuthenticationRequestWhenUnsignedPostThenOnlyPosts() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setPathInfo("/saml2/authenticate/registration-id"); + RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.assertingPartyDetails( + (party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST).wantAuthnRequestsSigned(false)) + .build(); + OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); + Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { + assertThat(authnRequest.getAssertionConsumerServiceURL()) + .isEqualTo(registration.getAssertionConsumerServiceLocation()); + assertThat(authnRequest.getProtocolBinding()) + .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); + assertThat(authnRequest.getDestination()) + .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); + }); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isNotNull(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(new String(Saml2Utils.samlDecode(result.getSamlRequest()))).doesNotContain("Signature"); + } + + @Test + public void resolveAuthenticationRequestWhenSignedPostThenSignsAndPosts() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setPathInfo("/saml2/authenticate/registration-id"); + RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder + .assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build(); + OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); + Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { + assertThat(authnRequest.getAssertionConsumerServiceURL()) + .isEqualTo(registration.getAssertionConsumerServiceLocation()); + assertThat(authnRequest.getProtocolBinding()) + .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); + assertThat(authnRequest.getDestination()) + .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); + }); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isNotNull(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(new String(Saml2Utils.samlDecode(result.getSamlRequest()))).contains("Signature"); + } + + @Test + public void resolveAuthenticationRequestWhenSHA1SignRequestThenSigns() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setPathInfo("/saml2/authenticate/registration-id"); + RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.assertingPartyDetails( + (party) -> party.signingAlgorithms((algs) -> algs.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1))) + .build(); + OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); + Saml2RedirectAuthenticationRequest result = resolver.resolve(request, null); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isNotNull(); + assertThat(result.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1); + assertThat(result.getSignature()).isNotNull(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + } + + private OpenSamlAuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistration registration) { + return new OpenSamlAuthenticationRequestResolver((request, id) -> registration); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java new file mode 100644 index 00000000000..acbbeb31eab --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication; + +import java.util.UUID; + +import javax.xml.namespace.QName; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.xmlsec.signature.Signature; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test open SAML signatures + */ +public class OpenSamlSigningUtilsTests { + + static { + OpenSamlInitializationService.initialize(); + } + + private RelyingPartyRegistration registration; + + @BeforeEach + public void setup() { + this.registration = RelyingPartyRegistration.withRegistrationId("saml-idp") + .entityId("https://some.idp.example.com/entity-id").signingX509Credentials((c) -> { + c.add(TestSaml2X509Credentials.relyingPartySigningCredential()); + c.add(TestSaml2X509Credentials.assertingPartySigningCredential()); + }).assertingPartyDetails((c) -> c.entityId("https://some.idp.example.com/entity-id") + .singleSignOnServiceLocation("https://some.idp.example.com/service-location")) + .build(); + } + + @Test + public void whenSigningAnObjectThenKeyInfoIsPartOfTheSignature() { + Response response = response("destination", "issuer"); + OpenSamlSigningUtils.sign(response, this.registration); + Signature signature = response.getSignature(); + assertThat(signature).isNotNull(); + assertThat(signature.getKeyInfo()).isNotNull(); + } + + Response response(String destination, String issuerEntityId) { + Response response = build(Response.DEFAULT_ELEMENT_NAME); + response.setID("R" + UUID.randomUUID()); + response.setVersion(SAMLVersion.VERSION_20); + response.setID("_" + UUID.randomUUID()); + response.setDestination(destination); + response.setIssuer(issuer(issuerEntityId)); + return response; + } + + Issuer issuer(String entityId) { + Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + return issuer; + } + + T build(QName qName) { + return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java index 7604e262f54..edc14ffab46 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import javax.servlet.http.HttpServletRequest; @@ -86,10 +87,13 @@ public void resolvePostWhenAuthenticatedThenIncludesName() { Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding); assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName()); + assertThat(logoutRequest.getSessionIndexes()).hasSize(1); + assertThat(logoutRequest.getSessionIndexes().get(0).getSessionIndex()).isEqualTo("session-index"); } private Saml2Authentication authentication(RelyingPartyRegistration registration) { - DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>(), + Arrays.asList("session-index")); principal.setRelyingPartyRegistrationId(registration.getRegistrationId()); return new Saml2Authentication(principal, "response", new ArrayList<>()); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java index b35d5181fe6..f2407e85dff 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java @@ -98,6 +98,29 @@ public void resolvePostWhenAuthenticatedThenSuccess() { 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()); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java new file mode 100644 index 00000000000..bf67994aef4 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.web.authentication.logout; + +import java.util.UUID; + +import javax.xml.namespace.QName; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.xmlsec.signature.Signature; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test open SAML signatures + */ +public class OpenSamlSigningUtilsTests { + + static { + OpenSamlInitializationService.initialize(); + } + + private RelyingPartyRegistration registration; + + @BeforeEach + public void setup() { + this.registration = RelyingPartyRegistration.withRegistrationId("saml-idp") + .entityId("https://some.idp.example.com/entity-id").signingX509Credentials((c) -> { + c.add(TestSaml2X509Credentials.relyingPartySigningCredential()); + c.add(TestSaml2X509Credentials.assertingPartySigningCredential()); + }).assertingPartyDetails((c) -> c.entityId("https://some.idp.example.com/entity-id") + .singleSignOnServiceLocation("https://some.idp.example.com/service-location")) + .build(); + } + + @Test + public void whenSigningAnObjectThenKeyInfoIsPartOfTheSignature() { + Response response = response("destination", "issuer"); + OpenSamlSigningUtils.sign(response, this.registration); + Signature signature = response.getSignature(); + assertThat(signature).isNotNull(); + assertThat(signature.getKeyInfo()).isNotNull(); + } + + Response response(String destination, String issuerEntityId) { + Response response = build(Response.DEFAULT_ELEMENT_NAME); + response.setID("R" + UUID.randomUUID()); + response.setVersion(SAMLVersion.VERSION_20); + response.setID("_" + UUID.randomUUID()); + response.setDestination(destination); + response.setIssuer(issuer(issuerEntityId)); + return response; + } + + Issuer issuer(String entityId) { + Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + return issuer; + } + + T build(QName qName) { + return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java index 2f08d6c122d..bb604c47759 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java @@ -153,4 +153,20 @@ public void doFilterWhenValidationFailsThen401() throws Exception { 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); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java index 2a86a06a26c..b201e52a05a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java @@ -37,6 +37,7 @@ 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 void doFilterWhenValidatorFailsThenStops() throws Exception { 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); + } + } diff --git a/saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml b/saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml new file mode 100644 index 00000000000..605c1973074 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml @@ -0,0 +1,168 @@ + + + + + + example.com + + + + Consortium GARR IdP + + + Consortium GARR IdP + + + + This Identity Provider gives support for the Consortium GARR's user community + + + Questo Identity Provider di test fornisce supporto alla comunita' utenti GARR + + + + + + + + + MIIDZjCCAk6gAwIBAgIVAL9O+PA7SXtlwZZY8MVSE9On1cVWMA0GCSqGSIb3DQEB + BQUAMCkxJzAlBgNVBAMTHmlkZW0tcHVwYWdlbnQuZG16LWludC51bmltby5pdDAe + Fw0xMzA3MjQwMDQ0MTRaFw0zMzA3MjQwMDQ0MTRaMCkxJzAlBgNVBAMTHmlkZW0t + cHVwYWdlbnQuZG16LWludC51bmltby5pdDCCASIwDQYJKoZIhvcNAMIIDQADggEP + ADCCAQoCggEBAIAcp/VyzZGXUF99kwj4NvL/Rwv4YvBgLWzpCuoxqHZ/hmBwJtqS + v0y9METBPFbgsF3hCISnxbcmNVxf/D0MoeKtw1YPbsUmow/bFe+r72hZ+IVAcejN + iDJ7t5oTjsRN1t1SqvVVk6Ryk5AZhpFW+W9pE9N6c7kJ16Rp2/mbtax9OCzxpece + byi1eiLfIBmkcRawL/vCc2v6VLI18i6HsNVO3l2yGosKCbuSoGDx2fCdAOk/rgdz + cWOvFsIZSKuD+FVbSS/J9GVs7yotsS4PRl4iX9UMnfDnOMfO7bcBgbXtDl4SCU1v + dJrRw7IL/pLz34Rv9a8nYitrzrxtLOp3nYUCAwEAAaOBhDCBgTBgBgMIIDEEWTBX + gh5pZGVtLXB1cGFnZW50LmRtei1pbnQudW5pbW8uaXSGNWh0dHBzOi8vaWRlbS1w + dXBhZ2VudC5kbXotaW50LnVuaW1vLml0L2lkcC9zaGliYm9sZXRoMB0GA1UdDgQW + BBT8PANzz+adGnTRe8ldcyxAwe4VnzANBgkqhkiG9w0BAQUFAAOCAQEAOEnO8Clu + 9z/Lf/8XOOsTdxJbV29DIF3G8KoQsB3dBsLwPZVEAQIP6ceS32Xaxrl6FMTDDNkL + qUvvInUisw0+I5zZwYHybJQCletUWTnz58SC4C9G7FpuXHFZnOGtRcgGD1NOX4UU + duus/4nVcGSLhDjszZ70Xtj0gw2Sn46oQPHTJ81QZ3Y9ih+Aj1c9OtUSBwtWZFkU + yooAKoR8li68Yb21zN2N65AqV+ndL98M8xUYMKLONuAXStDeoVCipH6PJ09Z5U2p + V5p4IQRV6QBsNw9CISJFuHzkVYTH5ZxzN80Ru46vh4y2M0Nu8GQ9I085KoZkrf5e + Cq53OZt9ISjHEw== + + + + + + + + + + + Consortium GARR + + + Consortium GARR + + + + Consortium GARR + + + Consortium GARR + + + + https://example.org + + + + + mailto:technical.contact@example.com + + + + + + + + example.org + + + + Consortium GARR IdP + + + Consortium GARR IdP + + + + This Identity Provider gives support for the Consortium GARR's user community + + + Questo Identity Provider di test fornisce supporto alla comunita' utenti GARR + + + + + + + + + MIIDZjCCAk6gAwIBAgIVAL9O+PA7SXtlwZZY8MVSE9On1cVWMA0GCSqGSIb3DQEB + BQUAMCkxJzAlBgNVBAMTHmlkZW0tcHVwYWdlbnQuZG16LWludC51bmltby5pdDAe + Fw0xMzA3MjQwMDQ0MTRaFw0zMzA3MjQwMDQ0MTRaMCkxJzAlBgNVBAMTHmlkZW0t + cHVwYWdlbnQuZG16LWludC51bmltby5pdDCCASIwDQYJKoZIhvcNAMIIDQADggEP + ADCCAQoCggEBAIAcp/VyzZGXUF99kwj4NvL/Rwv4YvBgLWzpCuoxqHZ/hmBwJtqS + v0y9METBPFbgsF3hCISnxbcmNVxf/D0MoeKtw1YPbsUmow/bFe+r72hZ+IVAcejN + iDJ7t5oTjsRN1t1SqvVVk6Ryk5AZhpFW+W9pE9N6c7kJ16Rp2/mbtax9OCzxpece + byi1eiLfIBmkcRawL/vCc2v6VLI18i6HsNVO3l2yGosKCbuSoGDx2fCdAOk/rgdz + cWOvFsIZSKuD+FVbSS/J9GVs7yotsS4PRl4iX9UMnfDnOMfO7bcBgbXtDl4SCU1v + dJrRw7IL/pLz34Rv9a8nYitrzrxtLOp3nYUCAwEAAaOBhDCBgTBgBgMIIDEEWTBX + gh5pZGVtLXB1cGFnZW50LmRtei1pbnQudW5pbW8uaXSGNWh0dHBzOi8vaWRlbS1w + dXBhZ2VudC5kbXotaW50LnVuaW1vLml0L2lkcC9zaGliYm9sZXRoMB0GA1UdDgQW + BBT8PANzz+adGnTRe8ldcyxAwe4VnzANBgkqhkiG9w0BAQUFAAOCAQEAOEnO8Clu + 9z/Lf/8XOOsTdxJbV29DIF3G8KoQsB3dBsLwPZVEAQIP6ceS32Xaxrl6FMTDDNkL + qUvvInUisw0+I5zZwYHybJQCletUWTnz58SC4C9G7FpuXHFZnOGtRcgGD1NOX4UU + duus/4nVcGSLhDjszZ70Xtj0gw2Sn46oQPHTJ81QZ3Y9ih+Aj1c9OtUSBwtWZFkU + yooAKoR8li68Yb21zN2N65AqV+ndL98M8xUYMKLONuAXStDeoVCipH6PJ09Z5U2p + V5p4IQRV6QBsNw9CISJFuHzkVYTH5ZxzN80Ru46vh4y2M0Nu8GQ9I085KoZkrf5e + Cq53OZt9ISjHEw== + + + + + + + + + + + Consortium GARR + + + Consortium GARR + + + + Consortium GARR + + + Consortium GARR + + + + https://example.org + + + + + mailto:technical.contact@example.org + + + + diff --git a/scripts/release/release-notes-sections.yml b/scripts/release/release-notes-sections.yml index 368a1a95316..0c9da756395 100644 --- a/scripts/release/release-notes-sections.yml +++ b/scripts/release/release-notes-sections.yml @@ -14,3 +14,6 @@ changelog: emoji: ":hammer:" labels: ["type: dependency-upgrade"] sort: "title" + issues: + exclude: + labels: ["status: duplicate"] diff --git a/settings.gradle b/settings.gradle index a73b597502b..e8e4fa76741 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,12 +6,10 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.6.1" + id "com.gradle.enterprise" version "3.8.1" id "io.spring.ge.conventions" version "0.0.7" } -enableFeaturePreview("VERSION_ORDERING_V2") - dependencyResolutionManagement { repositories { mavenCentral() diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index a45ad37f12a..587e83ffa7f 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -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" diff --git a/taglibs/src/main/resources/META-INF/security.tld b/taglibs/src/main/resources/META-INF/security.tld index b14bac5ab4c..5d5935140d2 100644 --- a/taglibs/src/main/resources/META-INF/security.tld +++ b/taglibs/src/main/resources/META-INF/security.tld @@ -20,7 +20,7 @@ version="2.0"> Spring Security Authorization Tag Library - 5.6 + 5.8 security http://www.springframework.org/security/tags diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index e5b977dda55..92b38684381 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -15,13 +15,13 @@ dependencies { optional 'org.springframework:spring-webmvc' optional 'org.springframework:spring-webflux' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path : ':spring-security-config', configuration : 'tests') testImplementation 'com.fasterxml.jackson.core:jackson-databind' testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java b/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java index 513723c1f13..323e1306803 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -58,8 +58,8 @@ else if (!(withUser.roles().length == 1 && "USER".equals(withUser.roles()[0]))) + " with authorities attribute " + Arrays.asList(withUser.authorities())); } User principal = new User(username, withUser.password(), true, true, true, true, grantedAuthorities); - Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), - principal.getAuthorities()); + Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(principal, + principal.getPassword(), principal.getAuthorities()); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); return context; diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java b/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java index cabc9e348bc..b9d6b7ce537 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -59,8 +59,8 @@ public SecurityContext createSecurityContext(WithUserDetails withUser) { String username = withUser.value(); Assert.hasLength(username, "value() must be non empty String"); UserDetails principal = userDetailsService.loadUserByUsername(username); - Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), - principal.getAuthorities()); + Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(principal, + principal.getPassword(), principal.getAuthorities()); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); return context; diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index c77a889f8f1..174a3188799 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -134,8 +134,8 @@ public static T mockA * @return the configurer to use */ public static T mockUser(UserDetails userDetails) { - return mockAuthentication(new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), - userDetails.getAuthorities())); + return mockAuthentication(UsernamePasswordAuthenticationToken.authenticated(userDetails, + userDetails.getPassword(), userDetails.getAuthorities())); } /** diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index 4d140655f59..33c2db2066c 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -41,8 +41,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import com.nimbusds.oauth2.sdk.util.StringUtils; - import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; @@ -102,6 +100,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -873,7 +872,7 @@ private static final class UserDetailsRequestPostProcessor implements RequestPos private final RequestPostProcessor delegate; UserDetailsRequestPostProcessor(UserDetails user) { - Authentication token = new UsernamePasswordAuthenticationToken(user, user.getPassword(), + Authentication token = UsernamePasswordAuthenticationToken.authenticated(user, user.getPassword(), user.getAuthorities()); this.delegate = new AuthenticationRequestPostProcessor(token); } @@ -1206,7 +1205,7 @@ private Collection defaultAuthorities() { return getAuthorities((Collection) scope); } String scopes = scope.toString(); - if (StringUtils.isBlank(scopes)) { + if (!StringUtils.hasText(scopes)) { return Collections.emptyList(); } return getAuthorities(Arrays.asList(scopes.split(" "))); diff --git a/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java b/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java index 8f9a79f51dc..c13ebdefe32 100644 --- a/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java +++ b/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java @@ -26,6 +26,7 @@ import org.springframework.security.config.BeanIds; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.csrf.CsrfFilter; @@ -61,10 +62,14 @@ private WebTestUtils() { */ public static SecurityContextRepository getSecurityContextRepository(HttpServletRequest request) { SecurityContextPersistenceFilter filter = findFilter(request, SecurityContextPersistenceFilter.class); - if (filter == null) { - return DEFAULT_CONTEXT_REPO; + if (filter != null) { + return (SecurityContextRepository) ReflectionTestUtils.getField(filter, "repo"); } - return (SecurityContextRepository) ReflectionTestUtils.getField(filter, "repo"); + SecurityContextHolderFilter holderFilter = findFilter(request, SecurityContextHolderFilter.class); + if (holderFilter != null) { + return (SecurityContextRepository) ReflectionTestUtils.getField(holderFilter, "securityContextRepository"); + } + return DEFAULT_CONTEXT_REPO; } /** @@ -80,6 +85,10 @@ public static void setSecurityContextRepository(HttpServletRequest request, if (filter != null) { ReflectionTestUtils.setField(filter, "repo", securityContextRepository); } + SecurityContextHolderFilter holderFilter = findFilter(request, SecurityContextHolderFilter.class); + if (holderFilter != null) { + ReflectionTestUtils.setField(holderFilter, "securityContextRepository", securityContextRepository); + } } /** diff --git a/test/src/test/java/org/springframework/security/test/context/showcase/WithMockCustomUserSecurityContextFactory.java b/test/src/test/java/org/springframework/security/test/context/showcase/WithMockCustomUserSecurityContextFactory.java index d174584cb1a..79f59ded86b 100644 --- a/test/src/test/java/org/springframework/security/test/context/showcase/WithMockCustomUserSecurityContextFactory.java +++ b/test/src/test/java/org/springframework/security/test/context/showcase/WithMockCustomUserSecurityContextFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -31,7 +31,7 @@ public class WithMockCustomUserSecurityContextFactory implements WithSecurityCon public SecurityContext createSecurityContext(WithMockCustomUser customUser) { SecurityContext context = SecurityContextHolder.createEmptyContext(); CustomUserDetails principal = new CustomUserDetails(customUser.name(), customUser.username()); - Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", + Authentication auth = UsernamePasswordAuthenticationToken.authenticated(principal, "password", principal.getAuthorities()); context.setAuthentication(auth); return context; diff --git a/test/src/test/java/org/springframework/security/test/web/support/WebTestUtilsTests.java b/test/src/test/java/org/springframework/security/test/web/support/WebTestUtilsTests.java index d8a1c6dee4b..c304202b4db 100644 --- a/test/src/test/java/org/springframework/security/test/web/support/WebTestUtilsTests.java +++ b/test/src/test/java/org/springframework/security/test/web/support/WebTestUtilsTests.java @@ -24,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.config.BeanIds; @@ -33,6 +34,7 @@ import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.csrf.CsrfFilter; @@ -43,6 +45,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) public class WebTestUtilsTests { @@ -126,6 +129,19 @@ public void getSecurityContextRepositorySecurityCustomRepo() { assertThat(WebTestUtils.getSecurityContextRepository(this.request)).isSameAs(this.contextRepo); } + @Test + public void setSecurityContextRepositoryWhenSecurityContextHolderFilter() { + SecurityContextRepository expectedRepository = mock(SecurityContextRepository.class); + loadConfig(SecurityContextHolderFilterConfig.class); + // verify our configuration sets up to have SecurityContextHolderFilter and not + // SecurityContextPersistenceFilter + assertThat(WebTestUtils.findFilter(this.request, SecurityContextPersistenceFilter.class)).isNull(); + assertThat(WebTestUtils.findFilter(this.request, SecurityContextHolderFilter.class)).isNotNull(); + + WebTestUtils.setSecurityContextRepository(this.request, expectedRepository); + assertThat(WebTestUtils.getSecurityContextRepository(this.request)).isSameAs(expectedRepository); + } + // gh-3343 @Test public void findFilterNoMatchingFilters() { @@ -220,4 +236,18 @@ static class SecurityConfigWithDefaults extends WebSecurityConfigurerAdapter { } + @EnableWebSecurity + static class SecurityContextHolderFilterConfig { + + @Bean + DefaultSecurityFilterChain springSecurityFilter(HttpSecurity http) throws Exception { + // @formatter:off + http + .securityContext((securityContext) -> securityContext.requireExplicitSave(true)); + // @formatter:on + return http.build(); + } + + } + } diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index ad2279b46e1..ca63924bd00 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -17,12 +17,11 @@ dependencies { optional 'org.springframework:spring-webflux' optional 'org.springframework:spring-webmvc' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-core', configuration: 'tests') - testImplementation 'commons-codec:commons-codec' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'org.hamcrest:hamcrest' testImplementation 'org.mockito:mockito-core' testImplementation 'org.mockito:mockito-inline' diff --git a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java index 1961bf84fd6..0191ee3e572 100644 --- a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java +++ b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,12 @@ public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filte } public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) { - logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); + if (filters.isEmpty()) { + logger.info(LogMessage.format("Will not secure %s", requestMatcher)); + } + else { + logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); + } this.requestMatcher = requestMatcher; this.filters = new ArrayList<>(filters); } diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java new file mode 100644 index 00000000000..b97debfa3e3 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; + +/** + * An implementation of {@link WebInvocationPrivilegeEvaluator} which delegates the checks + * to an instance of {@link AuthorizationManager} + * + * @author Marcus Da Coregio + * @since 5.5.5 + */ +public final class AuthorizationManagerWebInvocationPrivilegeEvaluator + implements WebInvocationPrivilegeEvaluator, ServletContextAware { + + private final AuthorizationManager authorizationManager; + + private ServletContext servletContext; + + public AuthorizationManagerWebInvocationPrivilegeEvaluator( + AuthorizationManager authorizationManager) { + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.authorizationManager = authorizationManager; + } + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return isAllowed(null, uri, null, authentication); + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method, this.servletContext); + AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, + filterInvocation.getHttpRequest()); + return decision == null || decision.isGranted(); + } + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java new file mode 100644 index 00000000000..e6e68b4fcfa --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; +import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; + +/** + * A {@link WebInvocationPrivilegeEvaluator} which delegates to a list of + * {@link WebInvocationPrivilegeEvaluator} based on a + * {@link org.springframework.security.web.util.matcher.RequestMatcher} evaluation + * + * @author Marcus Da Coregio + * @since 5.5.5 + */ +public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator + implements WebInvocationPrivilegeEvaluator, ServletContextAware { + + private final List>> delegates; + + private ServletContext servletContext; + + public RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + List>> requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(requestMatcherPrivilegeEvaluatorsEntries, "requestMatcherPrivilegeEvaluators cannot be null"); + for (RequestMatcherEntry> entry : requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(entry.getRequestMatcher(), "requestMatcher cannot be null"); + Assert.notNull(entry.getEntry(), "webInvocationPrivilegeEvaluators cannot be null"); + } + this.delegates = requestMatcherPrivilegeEvaluatorsEntries; + } + + /** + * Determines whether the user represented by the supplied Authentication + * object is allowed to invoke the supplied URI. + *

      + * Uses the provided URI in the + * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)} + * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is + * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator}, + * returns {@code true}. + * @param uri the URI excluding the context path (a default context path setting will + * be used) + * @return true if access is allowed, false if denied + */ + @Override + public boolean isAllowed(String uri, Authentication authentication) { + List privilegeEvaluators = getDelegate(null, uri, null); + if (privilegeEvaluators.isEmpty()) { + return true; + } + for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) { + boolean isAllowed = evaluator.isAllowed(uri, authentication); + if (!isAllowed) { + return false; + } + } + return true; + } + + /** + * Determines whether the user represented by the supplied Authentication + * object is allowed to invoke the supplied URI. + *

      + * Uses the provided URI in the + * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)} + * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is + * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator}, + * returns {@code true}. + * @param uri the URI excluding the context path (a default context path setting will + * be used) + * @param contextPath the context path (may be null, in which case a default value + * will be used). + * @param method the HTTP method (or null, for any method) + * @param authentication the Authentication instance whose authorities should + * be used in evaluation whether access should be granted. + * @return true if access is allowed, false if denied + */ + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + List privilegeEvaluators = getDelegate(contextPath, uri, method); + if (privilegeEvaluators.isEmpty()) { + return true; + } + for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) { + boolean isAllowed = evaluator.isAllowed(contextPath, uri, method, authentication); + if (!isAllowed) { + return false; + } + } + return true; + } + + private List getDelegate(String contextPath, String uri, String method) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method, this.servletContext); + for (RequestMatcherEntry> delegate : this.delegates) { + if (delegate.getRequestMatcher().matches(filterInvocation.getHttpRequest())) { + return delegate.getEntry(); + } + } + return Collections.emptyList(); + } + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java b/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java new file mode 100644 index 00000000000..a75c0d29592 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access.expression; + +import java.util.function.Supplier; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.access.expression.SecurityExpressionOperations; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.util.Assert; + +/** + * A {@link SecurityExpressionHandler} that uses a {@link RequestAuthorizationContext} to + * create a {@link WebSecurityExpressionRoot}. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpressionHandler + implements SecurityExpressionHandler { + + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + private String defaultRolePrefix = "ROLE_"; + + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, + RequestAuthorizationContext context) { + WebSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, context); + StandardEvaluationContext ctx = new StandardEvaluationContext(root); + ctx.setBeanResolver(getBeanResolver()); + context.getVariables().forEach(ctx::setVariable); + return ctx; + } + + @Override + protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, + RequestAuthorizationContext context) { + return createSecurityExpressionRoot(() -> authentication, context); + } + + private WebSecurityExpressionRoot createSecurityExpressionRoot(Supplier authentication, + RequestAuthorizationContext context) { + WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, context.getRequest()); + root.setRoleHierarchy(getRoleHierarchy()); + root.setPermissionEvaluator(getPermissionEvaluator()); + root.setTrustResolver(this.trustResolver); + root.setDefaultRolePrefix(this.defaultRolePrefix); + return root; + } + + /** + * Sets the {@link AuthenticationTrustResolver} to be used. The default is + * {@link AuthenticationTrustResolverImpl}. + * @param trustResolver the {@link AuthenticationTrustResolver} to use + */ + public void setTrustResolver(AuthenticationTrustResolver trustResolver) { + Assert.notNull(trustResolver, "trustResolver cannot be null"); + this.trustResolver = trustResolver; + } + + /** + * Sets the default prefix to be added to + * {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyRole(String...)} + * or + * {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasRole(String)}. + * For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN") is passed in, then the + * role ROLE_ADMIN will be used when the defaultRolePrefix is "ROLE_" (default). + * @param defaultRolePrefix the default prefix to add to roles. The default is + * "ROLE_". + */ + public void setDefaultRolePrefix(String defaultRolePrefix) { + Assert.hasText(defaultRolePrefix, "defaultRolePrefix cannot be empty"); + this.defaultRolePrefix = defaultRolePrefix; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/ExpressionAuthorizationDecision.java b/web/src/main/java/org/springframework/security/web/access/expression/ExpressionAuthorizationDecision.java new file mode 100644 index 00000000000..7095899d298 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/expression/ExpressionAuthorizationDecision.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access.expression; + +import org.springframework.expression.Expression; +import org.springframework.security.authorization.AuthorizationDecision; + +/** + * An expression-based {@link AuthorizationDecision}. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class ExpressionAuthorizationDecision extends AuthorizationDecision { + + private final Expression expression; + + /** + * Creates an instance. + * @param granted the decision to use + * @param expression the {@link Expression} to use + */ + public ExpressionAuthorizationDecision(boolean granted, Expression expression) { + super(granted); + this.expression = expression; + } + + /** + * Returns the {@link Expression}. + * @return the {@link Expression} to use + */ + public Expression getExpression() { + return this.expression; + } + + @Override + public String toString() { + return "ExpressionAuthorizationDecision[granted=" + isGranted() + ", expression='" + this.expression + "']"; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java new file mode 100644 index 00000000000..8b9ed3c4026 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access.expression; + +import java.util.function.Supplier; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.ExpressionUtils; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.util.Assert; + +/** + * An expression-based {@link AuthorizationManager} that determines the access by + * evaluating the provided expression. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class WebExpressionAuthorizationManager implements AuthorizationManager { + + private SecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler(); + + private Expression expression; + + /** + * Creates an instance. + * @param expressionString the raw expression string to parse + */ + public WebExpressionAuthorizationManager(String expressionString) { + Assert.hasText(expressionString, "expressionString cannot be empty"); + this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString); + } + + /** + * Sets the {@link SecurityExpressionHandler} to be used. The default is + * {@link DefaultHttpSecurityExpressionHandler}. + * @param expressionHandler the {@link SecurityExpressionHandler} to use + */ + public void setExpressionHandler(SecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + this.expression = expressionHandler.getExpressionParser() + .parseExpression(this.expression.getExpressionString()); + } + + /** + * Determines the access by evaluating the provided expression. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param context the {@link RequestAuthorizationContext} to check + * @return an {@link ExpressionAuthorizationDecision} based on the evaluated + * expression + */ + @Override + public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext context) { + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context); + boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx); + return new ExpressionAuthorizationDecision(granted, this.expression); + } + + @Override + public String toString() { + return "WebExpressionAuthorizationManager[expression='" + this.expression + "']"; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java b/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java index 91bc2df290b..516405f4eca 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.access.expression; +import java.util.function.Supplier; + import javax.servlet.http.HttpServletRequest; import org.springframework.security.access.expression.SecurityExpressionRoot; @@ -25,6 +27,7 @@ /** * @author Luke Taylor + * @author Evgeniy Cheban * @since 3.0 */ public class WebSecurityExpressionRoot extends SecurityExpressionRoot { @@ -35,8 +38,19 @@ public class WebSecurityExpressionRoot extends SecurityExpressionRoot { public final HttpServletRequest request; public WebSecurityExpressionRoot(Authentication a, FilterInvocation fi) { - super(a); - this.request = fi.getRequest(); + this(() -> a, fi.getRequest()); + } + + /** + * Creates an instance for the given {@link Supplier} of the {@link Authentication} + * and {@link HttpServletRequest}. + * @param authentication the {@link Supplier} of the {@link Authentication} to use + * @param request the {@link HttpServletRequest} to use + * @since 5.8 + */ + public WebSecurityExpressionRoot(Supplier authentication, HttpServletRequest request) { + super(authentication); + this.request = request; } /** diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java index 37b65ab0fbc..e772cf96f16 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java @@ -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. @@ -17,14 +17,21 @@ package org.springframework.security.web.access.intercept; import java.io.IOException; +import java.util.function.Supplier; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.event.AuthorizationDeniedEvent; +import org.springframework.security.authorization.event.AuthorizationGrantedEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.Assert; @@ -41,6 +48,10 @@ public class AuthorizationFilter extends OncePerRequestFilter { private final AuthorizationManager authorizationManager; + private AuthorizationEventPublisher eventPublisher = AuthorizationFilter::noPublish; + + private boolean shouldFilterAllDispatcherTypes = false; + /** * Creates an instance. * @param authorizationManager the {@link AuthorizationManager} to use @@ -54,7 +65,11 @@ public AuthorizationFilter(AuthorizationManager authorizatio protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - this.authorizationManager.verify(this::getAuthentication, request); + AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); + this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision); + if (decision != null && !decision.isGranted()) { + throw new AccessDeniedException("Access Denied"); + } filterChain.doFilter(request, response); } @@ -67,4 +82,54 @@ private Authentication getAuthentication() { return authentication; } + @Override + protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + doFilterInternal(request, response, filterChain); + } + + @Override + protected boolean shouldNotFilterAsyncDispatch() { + return !this.shouldFilterAllDispatcherTypes; + } + + @Override + protected boolean shouldNotFilterErrorDispatch() { + return !this.shouldFilterAllDispatcherTypes; + } + + /** + * Use this {@link AuthorizationEventPublisher} to publish + * {@link AuthorizationDeniedEvent}s and {@link AuthorizationGrantedEvent}s. + * @param eventPublisher the {@link ApplicationEventPublisher} to use + * @since 5.7 + */ + public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "eventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + } + + /** + * Gets the {@link AuthorizationManager} used by this filter + * @return the {@link AuthorizationManager} + */ + public AuthorizationManager getAuthorizationManager() { + return this.authorizationManager; + } + + /** + * Sets whether to filter all dispatcher types. + * @param shouldFilterAllDispatcherTypes should filter all dispatcher types. Default + * is {@code false} + * @since 5.7 + */ + public void setShouldFilterAllDispatcherTypes(boolean shouldFilterAllDispatcherTypes) { + this.shouldFilterAllDispatcherTypes = shouldFilterAllDispatcherTypes; + } + + private static void noPublish(Supplier authentication, T object, + AuthorizationDecision decision) { + + } + } diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java index b1c6378914f..e19285c1efe 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java @@ -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,8 +16,9 @@ package org.springframework.security.web.access.intercept; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; @@ -31,6 +32,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; /** @@ -38,16 +40,17 @@ * {@link AuthorizationManager} based on a {@link RequestMatcher} evaluation. * * @author Evgeniy Cheban + * @author Parikshit Dutta * @since 5.5 */ public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager { private final Log logger = LogFactory.getLog(getClass()); - private final Map> mappings; + private final List>> mappings; private RequestMatcherDelegatingAuthorizationManager( - Map> mappings) { + List>> mappings) { Assert.notEmpty(mappings, "mappings cannot be empty"); this.mappings = mappings; } @@ -66,13 +69,12 @@ public AuthorizationDecision check(Supplier authentication, Http if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Authorizing %s", request)); } - for (Map.Entry> mapping : this.mappings - .entrySet()) { + for (RequestMatcherEntry> mapping : this.mappings) { - RequestMatcher matcher = mapping.getKey(); + RequestMatcher matcher = mapping.getRequestMatcher(); MatchResult matchResult = matcher.matcher(request); if (matchResult.isMatch()) { - AuthorizationManager manager = mapping.getValue(); + AuthorizationManager manager = mapping.getEntry(); if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager)); } @@ -97,7 +99,7 @@ public static Builder builder() { */ public static final class Builder { - private final Map> mappings = new LinkedHashMap<>(); + private final List>> mappings = new ArrayList<>(); /** * Maps a {@link RequestMatcher} to an {@link AuthorizationManager}. @@ -108,7 +110,22 @@ public static final class Builder { public Builder add(RequestMatcher matcher, AuthorizationManager manager) { Assert.notNull(matcher, "matcher cannot be null"); Assert.notNull(manager, "manager cannot be null"); - this.mappings.put(matcher, manager); + this.mappings.add(new RequestMatcherEntry<>(matcher, manager)); + return this; + } + + /** + * Allows to configure the {@link RequestMatcher} to {@link AuthorizationManager} + * mappings. + * @param mappingsConsumer used to configure the {@link RequestMatcher} to + * {@link AuthorizationManager} mappings. + * @return the {@link Builder} for further customizations + * @since 5.7 + */ + public Builder mappings( + Consumer>>> mappingsConsumer) { + Assert.notNull(mappingsConsumer, "mappingsConsumer cannot be null"); + mappingsConsumer.accept(this.mappings); return this; } diff --git a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java index 933463c98ca..e8757d4d56e 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java @@ -42,6 +42,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -134,6 +136,8 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + /** * @param defaultFilterProcessesUrl the default value for filterProcessesUrl. */ @@ -314,6 +318,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } @@ -435,6 +440,18 @@ public void setAuthenticationFailureHandler(AuthenticationFailureHandler failure this.failureHandler = failureHandler; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + protected AuthenticationSuccessHandler getSuccessHandler() { return this.successHandler; } diff --git a/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java index fc42828a1d3..e8bec389c5c 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java @@ -32,6 +32,8 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -74,6 +76,8 @@ public class AuthenticationFilter extends OncePerRequestFilter { private AuthenticationFailureHandler failureHandler = new AuthenticationEntryPointFailureHandler( new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + private AuthenticationManagerResolver authenticationManagerResolver; public AuthenticationFilter(AuthenticationManager authenticationManager, @@ -135,6 +139,18 @@ public void setAuthenticationManagerResolver( this.authenticationManagerResolver = authenticationManagerResolver; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -173,6 +189,7 @@ private void successfulAuthentication(HttpServletRequest request, HttpServletRes SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); this.successHandler.onAuthenticationSuccess(request, response, chain, authentication); } diff --git a/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java b/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java new file mode 100644 index 00000000000..1f8618eb763 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; +import org.springframework.util.Assert; + +/** + * An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager} + * instances based upon the type of {@link HttpServletRequest} passed into + * {@link #resolve(HttpServletRequest)}. + * + * @author Josh Cummings + * @since 5.7 + */ +public final class RequestMatcherDelegatingAuthenticationManagerResolver + implements AuthenticationManagerResolver { + + private final List> authenticationManagers; + + private AuthenticationManager defaultAuthenticationManager = (authentication) -> { + throw new AuthenticationServiceException("Cannot authenticate " + authentication); + }; + + /** + * Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on + * the provided parameters + * @param authenticationManagers a {@link Map} of + * {@link RequestMatcher}/{@link AuthenticationManager} pairs + */ + RequestMatcherDelegatingAuthenticationManagerResolver( + RequestMatcherEntry... authenticationManagers) { + Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty"); + this.authenticationManagers = Arrays.asList(authenticationManagers); + } + + /** + * Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on + * the provided parameters + * @param authenticationManagers a {@link Map} of + * {@link RequestMatcher}/{@link AuthenticationManager} pairs + */ + RequestMatcherDelegatingAuthenticationManagerResolver( + List> authenticationManagers) { + Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty"); + this.authenticationManagers = authenticationManagers; + } + + /** + * {@inheritDoc} + */ + @Override + public AuthenticationManager resolve(HttpServletRequest context) { + for (RequestMatcherEntry entry : this.authenticationManagers) { + if (entry.getRequestMatcher().matches(context)) { + return entry.getEntry(); + } + } + + return this.defaultAuthenticationManager; + } + + /** + * Set the default {@link AuthenticationManager} to use when a request does not match + * @param defaultAuthenticationManager the default {@link AuthenticationManager} to + * use + */ + public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) { + Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null"); + this.defaultAuthenticationManager = defaultAuthenticationManager; + } + + /** + * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}. + * @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder} + * instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}. + */ + public static final class Builder { + + private final List> entries = new ArrayList<>(); + + private Builder() { + + } + + /** + * Maps a {@link RequestMatcher} to an {@link AuthorizationManager}. + * @param matcher the {@link RequestMatcher} to use + * @param manager the {@link AuthenticationManager} to use + * @return the {@link Builder} for further + * customizationServerWebExchangeDelegatingReactiveAuthenticationManagerResolvers + */ + public Builder add(RequestMatcher matcher, AuthenticationManager manager) { + Assert.notNull(matcher, "matcher cannot be null"); + Assert.notNull(manager, "manager cannot be null"); + this.entries.add(new RequestMatcherEntry<>(matcher, manager)); + return this; + } + + /** + * Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance. + * @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance + */ + public RequestMatcherDelegatingAuthenticationManagerResolver build() { + return new RequestMatcherDelegatingAuthenticationManagerResolver(this.entries); + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java index e1a444594fb..1b0fda5837e 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java @@ -75,11 +75,11 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); - username = (username != null) ? username : ""; - username = username.trim(); + username = (username != null) ? username.trim() : ""; String password = obtainPassword(request); password = (password != null) ? password : ""; - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); + UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, + password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); diff --git a/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java b/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java index 34586abd46c..8c53e5c717f 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java +++ b/web/src/main/java/org/springframework/security/web/authentication/WebAuthenticationDetails.java @@ -43,21 +43,25 @@ public class WebAuthenticationDetails implements Serializable { * @param request that the authentication request was received from */ public WebAuthenticationDetails(HttpServletRequest request) { - this.remoteAddress = request.getRemoteAddr(); - HttpSession session = request.getSession(false); - this.sessionId = (session != null) ? session.getId() : null; + this(request.getRemoteAddr(), extractSessionId(request)); } /** * Constructor to add Jackson2 serialize/deserialize support * @param remoteAddress remote address of current request * @param sessionId session id + * @since 5.7 */ - private WebAuthenticationDetails(final String remoteAddress, final String sessionId) { + public WebAuthenticationDetails(String remoteAddress, String sessionId) { this.remoteAddress = remoteAddress; this.sessionId = sessionId; } + private static String extractSessionId(HttpServletRequest request) { + HttpSession session = request.getSession(false); + return (session != null) ? session.getId() : null; + } + @Override public boolean equals(Object obj) { if (obj instanceof WebAuthenticationDetails) { diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java index 574e95de826..e05606f956c 100755 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java @@ -40,6 +40,8 @@ import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; @@ -104,6 +106,8 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi private RequestMatcher requiresAuthenticationRequestMatcher = new PreAuthenticatedProcessingRequestMatcher(); + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + /** * Check whether all required properties have been set. */ @@ -210,6 +214,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } @@ -242,6 +247,18 @@ public void setApplicationEventPublisher(ApplicationEventPublisher anApplication this.eventPublisher = anApplicationEventPublisher; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + /** * @param authenticationDetailsSource The AuthenticationDetailsSource to use */ diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java index d8573c0d70b..9e4a29999e0 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java @@ -36,6 +36,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; @@ -73,6 +75,8 @@ public class RememberMeAuthenticationFilter extends GenericFilterBean implements private RememberMeServices rememberMeServices; + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) { Assert.notNull(authenticationManager, "authenticationManager cannot be null"); @@ -114,6 +118,7 @@ private void doFilter(HttpServletRequest request, HttpServletResponse response, onSuccessfulAuthentication(request, response, rememberMeAuth); this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'")); + this.securityContextRepository.saveContext(context, request, response); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( SecurityContextHolder.getContext().getAuthentication(), this.getClass())); @@ -179,4 +184,16 @@ public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler success this.successHandler = successHandler; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + } diff --git a/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilter.java b/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilter.java index cd716fbfe5a..737aa6a9ea9 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilter.java @@ -297,7 +297,8 @@ private UsernamePasswordAuthenticationToken createSwitchUserToken(HttpServletReq List newAuths = new ArrayList<>(orig); newAuths.add(switchAuthority); // create the new authentication token - targetUserRequest = new UsernamePasswordAuthenticationToken(targetUser, targetUser.getPassword(), newAuths); + targetUserRequest = UsernamePasswordAuthenticationToken.authenticated(targetUser, targetUser.getPassword(), + newAuths); // set details targetUserRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); return targetUserRequest; diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverter.java index 2e39a676245..f7aae1c84aa 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -94,8 +94,8 @@ public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) { if (delim == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } - UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim), - token.substring(delim + 1)); + UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken + .unauthenticated(token.substring(0, delim), token.substring(delim + 1)); result.setDetails(this.authenticationDetailsSource.buildDetails(request)); return result; } diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java index 9d639adf151..208c94c54f5 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java @@ -36,6 +36,8 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.NullRememberMeServices; import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; @@ -103,6 +105,8 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + /** * Creates an instance which will authenticate against the supplied * {@code AuthenticationManager} and which will ignore failed authentication attempts, @@ -131,6 +135,18 @@ public BasicAuthenticationFilter(AuthenticationManager authenticationManager, this.authenticationEntryPoint = authenticationEntryPoint; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + @Override public void afterPropertiesSet() { Assert.notNull(this.authenticationManager, "An AuthenticationManager is required"); @@ -161,6 +177,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); + this.securityContextRepository.saveContext(context, request, response); onSuccessfulAuthentication(request, response, authResult); } } diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java index 21efe4f5f8e..3ea34697d83 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java @@ -49,6 +49,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.cache.NullUserCache; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; @@ -106,6 +108,8 @@ public class DigestAuthenticationFilter extends GenericFilterBean implements Mes private boolean createAuthenticatedToken = false; + private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); + @Override public void afterPropertiesSet() { Assert.notNull(this.userDetailsService, "A UserDetailsService is required"); @@ -192,6 +196,7 @@ private void doFilter(HttpServletRequest request, HttpServletResponse response, SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); chain.doFilter(request, response); } @@ -203,9 +208,9 @@ private Authentication createSuccessfulAuthentication(HttpServletRequest request private UsernamePasswordAuthenticationToken getAuthRequest(UserDetails user) { if (this.createAuthenticatedToken) { - return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); + return UsernamePasswordAuthenticationToken.authenticated(user, user.getPassword(), user.getAuthorities()); } - return new UsernamePasswordAuthenticationToken(user, user.getPassword()); + return UsernamePasswordAuthenticationToken.unauthenticated(user, user.getPassword()); } private void fail(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) @@ -271,6 +276,18 @@ public void setCreateAuthenticatedToken(boolean createAuthenticatedToken) { this.createAuthenticatedToken = createAuthenticatedToken; } + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + private class DigestData { private final String username; diff --git a/web/src/main/java/org/springframework/security/web/context/HttpRequestResponseHolder.java b/web/src/main/java/org/springframework/security/web/context/HttpRequestResponseHolder.java index d2e68ad8173..59376c1fafa 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpRequestResponseHolder.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpRequestResponseHolder.java @@ -27,7 +27,9 @@ * * @author Luke Taylor * @since 3.0 + * @deprecated Use {@link SecurityContextRepository#loadContext(HttpServletRequest)} */ +@Deprecated public final class HttpRequestResponseHolder { private HttpServletRequest request; diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 756db58a566..5bfff5684e0 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -123,10 +123,12 @@ public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHold this.logger.trace(LogMessage.format("Created %s", context)); } } - SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request, - httpSession != null, context); - requestResponseHolder.setResponse(wrappedResponse); - requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse)); + if (response != null) { + SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request, + httpSession != null, context); + requestResponseHolder.setResponse(wrappedResponse); + requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse)); + } return context; } @@ -134,8 +136,11 @@ public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHold public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class); - Assert.state(responseWrapper != null, () -> "Cannot invoke saveContext on response " + response - + ". You must use the HttpRequestResponseHolder.response after invoking loadContext"); + if (responseWrapper == null) { + boolean httpSessionExists = request.getSession(false) != null; + SecurityContext initialContext = SecurityContextHolder.createEmptyContext(); + responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext); + } responseWrapper.saveContext(context); } @@ -232,8 +237,11 @@ public void setSpringSecurityContextKey(String springSecurityContextKey) { this.springSecurityContextKey = springSecurityContextKey; } - private boolean isTransientAuthentication(Authentication authentication) { - return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null; + private boolean isTransient(Object object) { + if (object == null) { + return false; + } + return AnnotationUtils.getAnnotation(object.getClass(), Transient.class) != null; } /** @@ -326,7 +334,13 @@ final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorRespo */ @Override protected void saveContext(SecurityContext context) { + if (isTransient(context)) { + return; + } final Authentication authentication = context.getAuthentication(); + if (isTransient(authentication)) { + return; + } HttpSession httpSession = this.request.getSession(false); String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey; // See SEC-776 @@ -348,7 +362,7 @@ protected void saveContext(SecurityContext context) { } return; } - httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication); + httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context); // If HttpSession exists, store current SecurityContext but only if it has // actually changed in this thread (see SEC-37, SEC-1307, SEC-1528) if (httpSession != null) { @@ -369,10 +383,7 @@ private boolean contextChanged(SecurityContext context) { || context.getAuthentication() != this.authBeforeExecution; } - private HttpSession createNewSessionIfAllowed(SecurityContext context, Authentication authentication) { - if (isTransientAuthentication(authentication)) { - return null; - } + private HttpSession createNewSessionIfAllowed(SecurityContext context) { if (this.httpSessionExistedAtStartOfRequest) { this.logger.debug("HttpSession is now null, but was not null at start of request; " + "session was invalidated, so do not create a new session"); diff --git a/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java new file mode 100644 index 00000000000..d72045dfaf8 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.context; + +import java.util.function.Supplier; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Stores the {@link SecurityContext} on a + * {@link javax.servlet.ServletRequest#setAttribute(String, Object)} so that it can be + * restored when different dispatch types occur. It will not be available on subsequent + * requests. + * + * Unlike {@link HttpSessionSecurityContextRepository} this filter has no need to persist + * the {@link SecurityContext} on the response being committed because the + * {@link SecurityContext} will not be available for subsequent requests for + * {@link RequestAttributeSecurityContextRepository}. + * + * @author Rob Winch + * @since 5.7 + */ +public final class RequestAttributeSecurityContextRepository implements SecurityContextRepository { + + /** + * The default request attribute name to use. + */ + public static final String DEFAULT_REQUEST_ATTR_NAME = RequestAttributeSecurityContextRepository.class.getName() + .concat(".SPRING_SECURITY_CONTEXT"); + + private final String requestAttributeName; + + /** + * Creates a new instance using {@link #DEFAULT_REQUEST_ATTR_NAME}. + */ + public RequestAttributeSecurityContextRepository() { + this(DEFAULT_REQUEST_ATTR_NAME); + } + + /** + * Creates a new instance with the specified request attribute name. + * @param requestAttributeName the request attribute name to set to the + * {@link SecurityContext}. + */ + public RequestAttributeSecurityContextRepository(String requestAttributeName) { + this.requestAttributeName = requestAttributeName; + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return loadContext(request).get() != null; + } + + @Override + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + SecurityContext context = loadContext(requestResponseHolder.getRequest()).get(); + return (context != null) ? context : SecurityContextHolder.createEmptyContext(); + } + + @Override + public Supplier loadContext(HttpServletRequest request) { + return () -> (SecurityContext) request.getAttribute(this.requestAttributeName); + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + request.setAttribute(this.requestAttributeName, context); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java b/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java index 4fb534a5655..94925e877ed 100644 --- a/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java +++ b/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java @@ -16,6 +16,7 @@ package org.springframework.security.web.context; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.context.SecurityContext; @@ -39,7 +40,10 @@ * @author Marten Algesten * @author Rob Winch * @since 3.0 + * @deprecated Use {@link SecurityContextRepository#loadContext(HttpServletRequest)} + * instead. */ +@Deprecated public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends OnCommittedResponseWrapper { private boolean contextSaved = false; diff --git a/web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java b/web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java new file mode 100644 index 00000000000..b8e10cc2e4c --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.context; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * A {@link javax.servlet.Filter} that uses the {@link SecurityContextRepository} to + * obtain the {@link SecurityContext} and set it on the {@link SecurityContextHolder}. + * This is similar to {@link SecurityContextPersistenceFilter} except that the + * {@link SecurityContextRepository#saveContext(SecurityContext, HttpServletRequest, HttpServletResponse)} + * must be explicitly invoked to save the {@link SecurityContext}. This improves the + * efficiency and provides better flexibility by allowing different authentication + * mechanisms to choose individually if authentication should be persisted. + * + * @author Rob Winch + * @since 5.7 + */ +public class SecurityContextHolderFilter extends OncePerRequestFilter { + + private final SecurityContextRepository securityContextRepository; + + private boolean shouldNotFilterErrorDispatch; + + /** + * Creates a new instance. + * @param securityContextRepository the repository to use. Cannot be null. + */ + public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + SecurityContext securityContext = this.securityContextRepository.loadContext(request).get(); + try { + SecurityContextHolder.setContext(securityContext); + filterChain.doFilter(request, response); + } + finally { + SecurityContextHolder.clearContext(); + } + } + + @Override + protected boolean shouldNotFilterErrorDispatch() { + return this.shouldNotFilterErrorDispatch; + } + + /** + * Disables {@link SecurityContextHolderFilter} for error dispatch. + * @param shouldNotFilterErrorDispatch if the Filter should be disabled for error + * dispatch. Default is false. + */ + public void setShouldNotFilterErrorDispatch(boolean shouldNotFilterErrorDispatch) { + this.shouldNotFilterErrorDispatch = shouldNotFilterErrorDispatch; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/context/SecurityContextPersistenceFilter.java b/web/src/main/java/org/springframework/security/web/context/SecurityContextPersistenceFilter.java index 9a369a2161e..0e7f869aac3 100644 --- a/web/src/main/java/org/springframework/security/web/context/SecurityContextPersistenceFilter.java +++ b/web/src/main/java/org/springframework/security/web/context/SecurityContextPersistenceFilter.java @@ -57,7 +57,9 @@ * * @author Luke Taylor * @since 3.0 + * @deprecated Use {@link SecurityContextHolderFilter} */ +@Deprecated public class SecurityContextPersistenceFilter extends GenericFilterBean { static final String FILTER_APPLIED = "__spring_security_scpf_applied"; diff --git a/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java index 506dda8e38f..1e1805c81dc 100644 --- a/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java @@ -16,6 +16,8 @@ package org.springframework.security.web.context; +import java.util.function.Supplier; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -48,16 +50,35 @@ public interface SecurityContextRepository { * to return wrapped versions of the request or response (or both), allowing them to * access implementation-specific state for the request. The values obtained from the * holder will be passed on to the filter chain and also to the saveContext - * method when it is finally called. Implementations may wish to return a subclass of + * method when it is finally called to allow implicit saves of the + * SecurityContext. Implementations may wish to return a subclass of * {@link SaveContextOnUpdateOrErrorResponseWrapper} as the response object, which * guarantees that the context is persisted when an error or redirect occurs. + * Implementations may allow passing in the original request response to allow + * explicit saves. * @param requestResponseHolder holder for the current request and response for which * the context should be loaded. * @return The security context which should be used for the current request, never * null. + * @deprecated Use {@link #loadContext(HttpServletRequest)} instead. */ + @Deprecated SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder); + /** + * Obtains the security context for the supplied request. For an unauthenticated user, + * an empty context implementation should be returned. This method should not return + * null. + * @param request the {@link HttpServletRequest} to load the {@link SecurityContext} + * from + * @return a {@link Supplier} that returns the {@link SecurityContext} which cannot be + * null. + * @since 5.7 + */ + default Supplier loadContext(HttpServletRequest request) { + return () -> loadContext(new HttpRequestResponseHolder(request, null)); + } + /** * Stores the security context on completion of a request. * @param context the non-null context which was obtained from the holder. diff --git a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java index a25b7927ace..8c4a3aff48c 100644 --- a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java +++ b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,6 +151,10 @@ public void init(FilterConfig filterConfig) { public void destroy() { } + public FilterChainProxy getFilterChainProxy() { + return this.filterChainProxy; + } + static class DebugRequestWrapper extends HttpServletRequestWrapper { private static final Logger logger = new Logger(); diff --git a/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java b/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java new file mode 100644 index 00000000000..81013b6bcdf --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.firewall; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.Assert; + +/** + * A {@link RequestRejectedHandler} that delegates to several other + * {@link RequestRejectedHandler}s. + * + * @author Adam Ostrožlík + * @since 5.7 + */ +public final class CompositeRequestRejectedHandler implements RequestRejectedHandler { + + private final List requestRejectedhandlers; + + /** + * Creates a new instance. + * @param requestRejectedhandlers the {@link RequestRejectedHandler} instances to + * handle {@link org.springframework.security.web.firewall.RequestRejectedException} + */ + public CompositeRequestRejectedHandler(RequestRejectedHandler... requestRejectedhandlers) { + Assert.notEmpty(requestRejectedhandlers, "requestRejectedhandlers cannot be empty"); + this.requestRejectedhandlers = Arrays.asList(requestRejectedhandlers); + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + RequestRejectedException requestRejectedException) throws IOException, ServletException { + for (RequestRejectedHandler requestRejectedhandler : this.requestRejectedhandlers) { + requestRejectedhandler.handle(request, response, requestRejectedException); + } + } + +} diff --git a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java index 282184b3b38..c6e566a0c3b 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java +++ b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java @@ -107,6 +107,15 @@ public class StrictHttpFirewall implements HttpFirewall { private static final List FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00")); + private static final List FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A")); + + private static final List FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\r", "%0d", "%0D")); + + private static final List FORBIDDEN_LINE_SEPARATOR = Collections.unmodifiableList(Arrays.asList("\u2028")); + + private static final List FORBIDDEN_PARAGRAPH_SEPARATOR = Collections + .unmodifiableList(Arrays.asList("\u2029")); + private Set encodedUrlBlocklist = new HashSet<>(); private Set decodedUrlBlocklist = new HashSet<>(); @@ -135,10 +144,14 @@ public StrictHttpFirewall() { urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH); urlBlocklistsAddAll(FORBIDDEN_BACKSLASH); urlBlocklistsAddAll(FORBIDDEN_NULL); + urlBlocklistsAddAll(FORBIDDEN_LF); + urlBlocklistsAddAll(FORBIDDEN_CR); this.encodedUrlBlocklist.add(ENCODED_PERCENT); this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD); this.decodedUrlBlocklist.add(PERCENT); + this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR); + this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR); } /** @@ -345,6 +358,69 @@ public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) { } } + /** + * Determines if a URL encoded Carriage Return is allowed in the path or not. The + * default is not to allow this behavior because it is a frequent source of security + * exploits. + * @param allowUrlEncodedCarriageReturn if URL encoded Carriage Return is allowed in + * the URL or not. Default is false. + */ + public void setAllowUrlEncodedCarriageReturn(boolean allowUrlEncodedCarriageReturn) { + if (allowUrlEncodedCarriageReturn) { + urlBlocklistsRemoveAll(FORBIDDEN_CR); + } + else { + urlBlocklistsAddAll(FORBIDDEN_CR); + } + } + + /** + * Determines if a URL encoded Line Feed is allowed in the path or not. The default is + * not to allow this behavior because it is a frequent source of security exploits. + * @param allowUrlEncodedLineFeed if URL encoded Line Feed is allowed in the URL or + * not. Default is false. + */ + public void setAllowUrlEncodedLineFeed(boolean allowUrlEncodedLineFeed) { + if (allowUrlEncodedLineFeed) { + urlBlocklistsRemoveAll(FORBIDDEN_LF); + } + else { + urlBlocklistsAddAll(FORBIDDEN_LF); + } + } + + /** + * Determines if a URL encoded paragraph separator is allowed in the path or not. The + * default is not to allow this behavior because it is a frequent source of security + * exploits. + * @param allowUrlEncodedParagraphSeparator if URL encoded paragraph separator is + * allowed in the URL or not. Default is false. + */ + public void setAllowUrlEncodedParagraphSeparator(boolean allowUrlEncodedParagraphSeparator) { + if (allowUrlEncodedParagraphSeparator) { + this.decodedUrlBlocklist.removeAll(FORBIDDEN_PARAGRAPH_SEPARATOR); + } + else { + this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR); + } + } + + /** + * Determines if a URL encoded line separator is allowed in the path or not. The + * default is not to allow this behavior because it is a frequent source of security + * exploits. + * @param allowUrlEncodedLineSeparator if URL encoded line separator is allowed in the + * URL or not. Default is false. + */ + public void setAllowUrlEncodedLineSeparator(boolean allowUrlEncodedLineSeparator) { + if (allowUrlEncodedLineSeparator) { + this.decodedUrlBlocklist.removeAll(FORBIDDEN_LINE_SEPARATOR); + } + else { + this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR); + } + } + /** *

      * Determines which header names should be allowed. The default is to reject header @@ -431,14 +507,17 @@ public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws if (!isNormalized(request)) { throw new RequestRejectedException("The request was rejected because the URL was not normalized."); } - String requestUri = request.getRequestURI(); - if (!containsOnlyPrintableAsciiCharacters(requestUri)) { - throw new RequestRejectedException( - "The requestURI was rejected because it can only contain printable ASCII characters."); - } + rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI"); return new StrictFirewalledRequest(request); } + private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) { + if (!containsOnlyPrintableAsciiCharacters(toCheck)) { + throw new RequestRejectedException(String.format( + "The %s was rejected because it can only contain printable ASCII characters.", propertyName)); + } + } + private void rejectForbiddenHttpMethod(HttpServletRequest request) { if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) { return; @@ -526,6 +605,9 @@ private static boolean decodedUrlContains(HttpServletRequest request, String val } private static boolean containsOnlyPrintableAsciiCharacters(String uri) { + if (uri == null) { + return true; + } int length = uri.length(); for (int i = 0; i < length; i++) { char ch = uri.charAt(i); diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java new file mode 100644 index 00000000000..5ec60c051ed --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Inserts Cross-Origin-Embedder-Policy header. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Embedder-Policy + */ +public final class CrossOriginEmbedderPolicyHeaderWriter implements HeaderWriter { + + private static final String EMBEDDER_POLICY = "Cross-Origin-Embedder-Policy"; + + private CrossOriginEmbedderPolicy policy; + + /** + * Sets the {@link CrossOriginEmbedderPolicy} value to be used in the + * {@code Cross-Origin-Embedder-Policy} header + * @param embedderPolicy the {@link CrossOriginEmbedderPolicy} to use + */ + public void setPolicy(CrossOriginEmbedderPolicy embedderPolicy) { + Assert.notNull(embedderPolicy, "embedderPolicy cannot be null"); + this.policy = embedderPolicy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.policy != null && !response.containsHeader(EMBEDDER_POLICY)) { + response.addHeader(EMBEDDER_POLICY, this.policy.getPolicy()); + } + } + + public enum CrossOriginEmbedderPolicy { + + UNSAFE_NONE("unsafe-none"), + + REQUIRE_CORP("require-corp"); + + private final String policy; + + CrossOriginEmbedderPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + public static CrossOriginEmbedderPolicy from(String embedderPolicy) { + for (CrossOriginEmbedderPolicy policy : values()) { + if (policy.getPolicy().equals(embedderPolicy)) { + return policy; + } + } + return null; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java new file mode 100644 index 00000000000..182a62c414b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Inserts the Cross-Origin-Opener-Policy header + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Opener-Policy + */ +public final class CrossOriginOpenerPolicyHeaderWriter implements HeaderWriter { + + private static final String OPENER_POLICY = "Cross-Origin-Opener-Policy"; + + private CrossOriginOpenerPolicy policy; + + /** + * Sets the {@link CrossOriginOpenerPolicy} value to be used in the + * {@code Cross-Origin-Opener-Policy} header + * @param openerPolicy the {@link CrossOriginOpenerPolicy} to use + */ + public void setPolicy(CrossOriginOpenerPolicy openerPolicy) { + Assert.notNull(openerPolicy, "openerPolicy cannot be null"); + this.policy = openerPolicy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.policy != null && !response.containsHeader(OPENER_POLICY)) { + response.addHeader(OPENER_POLICY, this.policy.getPolicy()); + } + } + + public enum CrossOriginOpenerPolicy { + + UNSAFE_NONE("unsafe-none"), + + SAME_ORIGIN_ALLOW_POPUPS("same-origin-allow-popups"), + + SAME_ORIGIN("same-origin"); + + private final String policy; + + CrossOriginOpenerPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + public static CrossOriginOpenerPolicy from(String openerPolicy) { + for (CrossOriginOpenerPolicy policy : values()) { + if (policy.getPolicy().equals(openerPolicy)) { + return policy; + } + } + return null; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java new file mode 100644 index 00000000000..d454ce780ab --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Inserts Cross-Origin-Resource-Policy header + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Resource-Policy + */ +public final class CrossOriginResourcePolicyHeaderWriter implements HeaderWriter { + + private static final String RESOURCE_POLICY = "Cross-Origin-Resource-Policy"; + + private CrossOriginResourcePolicy policy; + + /** + * Sets the {@link CrossOriginResourcePolicy} value to be used in the + * {@code Cross-Origin-Resource-Policy} header + * @param resourcePolicy the {@link CrossOriginResourcePolicy} to use + */ + public void setPolicy(CrossOriginResourcePolicy resourcePolicy) { + Assert.notNull(resourcePolicy, "resourcePolicy cannot be null"); + this.policy = resourcePolicy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.policy != null && !response.containsHeader(RESOURCE_POLICY)) { + response.addHeader(RESOURCE_POLICY, this.policy.getPolicy()); + } + } + + public enum CrossOriginResourcePolicy { + + SAME_SITE("same-site"), + + SAME_ORIGIN("same-origin"), + + CROSS_ORIGIN("cross-origin"); + + private final String policy; + + CrossOriginResourcePolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + public static CrossOriginResourcePolicy from(String resourcePolicy) { + for (CrossOriginResourcePolicy policy : values()) { + if (policy.getPolicy().equals(resourcePolicy)) { + return policy; + } + } + return null; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java index ea2358293f6..1655190d018 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * 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. @@ -33,6 +33,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.web.PortResolver; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; @@ -61,6 +62,8 @@ */ public class DefaultSavedRequest implements SavedRequest { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + protected static final Log logger = LogFactory.getLog(DefaultSavedRequest.class); private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java b/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java index 9357e98fbed..471863bf2c6 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -20,6 +20,8 @@ import javax.servlet.http.Cookie; +import org.springframework.security.core.SpringSecurityCoreVersion; + /** * Stores off the values of a cookie in a serializable holder * @@ -27,6 +29,8 @@ */ public class SavedCookie implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private final java.lang.String name; private final java.lang.String value; diff --git a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java index 9d538ad0aa4..01fa28c6b09 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -52,7 +52,7 @@ public Mono apply(ServerWebExchange exchange) { private UsernamePasswordAuthenticationToken createAuthentication(MultiValueMap data) { String username = data.getFirst(this.usernameParameter); String password = data.getFirst(this.passwordParameter); - return new UsernamePasswordAuthenticationToken(username, password); + return UsernamePasswordAuthenticationToken.unauthenticated(username, password); } /** diff --git a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java index 3f58b31ec7c..7494fefa902 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.server; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.function.Function; @@ -25,6 +27,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @@ -43,6 +46,8 @@ public class ServerHttpBasicAuthenticationConverter implements Function apply(ServerWebExchange exchange) { @@ -51,14 +56,13 @@ public Mono apply(ServerWebExchange exchange) { if (!StringUtils.startsWithIgnoreCase(authorization, "basic ")) { return Mono.empty(); } - String credentials = (authorization.length() <= BASIC.length()) ? "" - : authorization.substring(BASIC.length(), authorization.length()); - String decoded = new String(base64Decode(credentials)); + String credentials = (authorization.length() <= BASIC.length()) ? "" : authorization.substring(BASIC.length()); + String decoded = new String(base64Decode(credentials), this.credentialsCharset); String[] parts = decoded.split(":", 2); if (parts.length != 2) { return Mono.empty(); } - return Mono.just(new UsernamePasswordAuthenticationToken(parts[0], parts[1])); + return Mono.just(UsernamePasswordAuthenticationToken.unauthenticated(parts[0], parts[1])); } private byte[] base64Decode(String value) { @@ -70,4 +74,16 @@ private byte[] base64Decode(String value) { } } + /** + * Sets the {@link Charset} used to decode the Base64-encoded bytes of the basic + * authentication credentials. The default is UTF_8. + * @param credentialsCharset the {@link Charset} used to decode the Base64-encoded + * bytes of the basic authentication credentials + * @since 5.7 + */ + public final void setCredentialsCharset(Charset credentialsCharset) { + Assert.notNull(credentialsCharset, "credentialsCharset cannot be null"); + this.credentialsCharset = credentialsCharset; + } + } diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java b/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java new file mode 100644 index 00000000000..a28facf95b3 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; +import org.springframework.security.web.authentication.RequestMatcherDelegatingAuthenticationManagerResolver; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ReactiveAuthenticationManagerResolver} that returns a + * {@link ReactiveAuthenticationManager} instances based upon the type of + * {@link ServerWebExchange} passed into {@link #resolve(ServerWebExchange)}. + * + * @author Josh Cummings + * @since 5.7 + * + */ +public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver + implements ReactiveAuthenticationManagerResolver { + + private final List> authenticationManagers; + + private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono + .error(new AuthenticationServiceException("Cannot authenticate " + authentication)); + + /** + * Construct an + * {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on + * the provided parameters + * @param managers a set of {@link ServerWebExchangeMatcherEntry}s + */ + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver( + ServerWebExchangeMatcherEntry... managers) { + this(Arrays.asList(managers)); + } + + /** + * Construct an + * {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on + * the provided parameters + * @param managers a {@link List} of {@link ServerWebExchangeMatcherEntry}s + */ + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver( + List> managers) { + Assert.notNull(managers, "entries cannot be null"); + this.authenticationManagers = managers; + } + + /** + * {@inheritDoc} + */ + @Override + public Mono resolve(ServerWebExchange exchange) { + return Flux.fromIterable(this.authenticationManagers).filterWhen((entry) -> isMatch(exchange, entry)).next() + .map(ServerWebExchangeMatcherEntry::getEntry).defaultIfEmpty(this.defaultAuthenticationManager); + } + + /** + * Set the default {@link ReactiveAuthenticationManager} to use when a request does + * not match + * @param defaultAuthenticationManager the default + * {@link ReactiveAuthenticationManager} to use + */ + public void setDefaultAuthenticationManager(ReactiveAuthenticationManager defaultAuthenticationManager) { + Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null"); + this.defaultAuthenticationManager = defaultAuthenticationManager; + } + + /** + * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}. + * @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder} + * instance + */ + public static Builder builder() { + return new Builder(); + } + + private Mono isMatch(ServerWebExchange exchange, + ServerWebExchangeMatcherEntry entry) { + ServerWebExchangeMatcher matcher = entry.getMatcher(); + return matcher.matches(exchange).map(ServerWebExchangeMatcher.MatchResult::isMatch); + } + + /** + * A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}. + */ + public static final class Builder { + + private final List> entries = new ArrayList<>(); + + private Builder() { + + } + + /** + * Maps a {@link ServerWebExchangeMatcher} to an + * {@link ReactiveAuthenticationManager}. + * @param matcher the {@link ServerWebExchangeMatcher} to use + * @param manager the {@link ReactiveAuthenticationManager} to use + * @return the + * {@link RequestMatcherDelegatingAuthenticationManagerResolver.Builder} for + * further customizations + */ + public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder add( + ServerWebExchangeMatcher matcher, ReactiveAuthenticationManager manager) { + Assert.notNull(matcher, "matcher cannot be null"); + Assert.notNull(manager, "manager cannot be null"); + this.entries.add(new ServerWebExchangeMatcherEntry<>(matcher, manager)); + return this; + } + + /** + * Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance. + * @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance + */ + public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver build() { + return new ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(this.entries); + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java index 000b0caaa40..cb1ae04807f 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java @@ -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. @@ -261,7 +261,7 @@ private Authentication createSwitchUserToken(UserDetails targetUser, Authenticat Collection targetUserAuthorities = targetUser.getAuthorities(); List extendedTargetUserAuthorities = new ArrayList<>(targetUserAuthorities); extendedTargetUserAuthorities.add(switchAuthority); - return new UsernamePasswordAuthenticationToken(targetUser, targetUser.getPassword(), + return UsernamePasswordAuthenticationToken.authenticated(targetUser, targetUser.getPassword(), extendedTargetUserAuthorities); } diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java index b83663fc82c..0c7acb15967 100644 --- a/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -20,7 +20,6 @@ import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; -import org.springframework.context.support.MessageSourceAccessor; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; @@ -29,7 +28,6 @@ import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; import org.springframework.util.Assert; @@ -51,8 +49,6 @@ public class ExceptionTranslationWebFilter implements WebFilter, MessageSourceAw private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); - protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); - @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange).onErrorResume(AccessDeniedException.class, (denied) -> exchange.getPrincipal() @@ -60,8 +56,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { && !(this.authenticationTrustResolver.isAnonymous((Authentication) principal))))) .switchIfEmpty(commenceAuthentication(exchange, new InsufficientAuthenticationException( - this.messages.getMessage("ExceptionTranslationWebFilter.insufficientAuthentication", - "Full authentication is required to access this resource")))) + "Full authentication is required to access this resource"))) .flatMap((principal) -> this.accessDeniedHandler.handle(exchange, denied)).then()); } @@ -99,11 +94,10 @@ public void setAuthenticationTrustResolver(AuthenticationTrustResolver authentic /** * @since 5.5 + * @deprecated This class no longer retrieves error messages from a MessageSource */ - @Override + @Deprecated public void setMessageSource(MessageSource messageSource) { - Assert.notNull(messageSource, "messageSource cannot be null"); - this.messages = new MessageSourceAccessor(messageSource); } private Mono commenceAuthentication(ServerWebExchange exchange, AuthenticationException denied) { diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java new file mode 100644 index 00000000000..5b814a52761 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authorization; + +import reactor.core.publisher.Mono; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.util.matcher.IpAddressServerWebExchangeMatcher; +import org.springframework.util.Assert; + +/** + * A {@link ReactiveAuthorizationManager}, that determines if the current request contains + * the specified address or range of addresses + * + * @author Guirong Hu + * @since 5.7 + */ +public final class IpAddressReactiveAuthorizationManager implements ReactiveAuthorizationManager { + + private final IpAddressServerWebExchangeMatcher ipAddressExchangeMatcher; + + IpAddressReactiveAuthorizationManager(String ipAddress) { + this.ipAddressExchangeMatcher = new IpAddressServerWebExchangeMatcher(ipAddress); + } + + @Override + public Mono check(Mono authentication, AuthorizationContext context) { + return Mono.just(context.getExchange()).flatMap(this.ipAddressExchangeMatcher::matches) + .map((matchResult) -> new AuthorizationDecision(matchResult.isMatch())); + } + + /** + * Creates an instance of {@link IpAddressReactiveAuthorizationManager} with the + * provided IP address. + * @param ipAddress the address or range of addresses from which the request must + * @return the new instance + */ + public static IpAddressReactiveAuthorizationManager hasIpAddress(String ipAddress) { + Assert.notNull(ipAddress, "This IP address is required; it must not be null"); + return new IpAddressReactiveAuthorizationManager(ipAddress); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java index 3bd3ed8b8a4..a70b4908f55 100644 --- a/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java @@ -46,6 +46,8 @@ public class WebSessionServerSecurityContextRepository implements ServerSecurity private String springSecurityContextAttrName = DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME; + private boolean cacheSecurityContext; + /** * Sets the session attribute name used to save and load the {@link SecurityContext} * @param springSecurityContextAttrName the session attribute name to use to save and @@ -56,6 +58,16 @@ public void setSpringSecurityContextAttrName(String springSecurityContextAttrNam this.springSecurityContextAttrName = springSecurityContextAttrName; } + /** + * If set to true the result of {@link #load(ServerWebExchange)} will use + * {@link Mono#cache()} to prevent multiple lookups. + * @param cacheSecurityContext true if {@link Mono#cache()} should be used, else + * false. + */ + public void setCacheSecurityContext(boolean cacheSecurityContext) { + this.cacheSecurityContext = cacheSecurityContext; + } + @Override public Mono save(ServerWebExchange exchange, SecurityContext context) { return exchange.getSession().doOnNext((session) -> { @@ -72,13 +84,14 @@ public Mono save(ServerWebExchange exchange, SecurityContext context) { @Override public Mono load(ServerWebExchange exchange) { - return exchange.getSession().flatMap((session) -> { + Mono result = exchange.getSession().flatMap((session) -> { SecurityContext context = (SecurityContext) session.getAttribute(this.springSecurityContextAttrName); logger.debug((context != null) ? LogMessage.format("Found SecurityContext '%s' in WebSession: '%s'", context, session) : LogMessage.format("No SecurityContext found in WebSession: '%s'", session)); return Mono.justOrEmpty(context); }); + return (this.cacheSecurityContext) ? result.cache() : result; } } diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java b/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java index 718ccdf41cf..241ad767b61 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java @@ -151,7 +151,7 @@ private Mono tokenFromMultipartData(ServerWebExchange exchange, CsrfToke ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); - if (!contentType.includes(MediaType.MULTIPART_FORM_DATA)) { + if (!MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) { return Mono.empty(); } return exchange.getMultipartData().map((d) -> d.getFirst(expected.getParameterName())).cast(FormFieldPart.class) diff --git a/web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java new file mode 100644 index 00000000000..17446845dde --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Inserts Cross-Origin-Embedder-Policy headers. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Embedder-Policy + */ +public final class CrossOriginEmbedderPolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String EMBEDDER_POLICY = "Cross-Origin-Embedder-Policy"; + + private ServerHttpHeadersWriter delegate; + + /** + * Sets the {@link CrossOriginEmbedderPolicy} value to be used in the + * {@code Cross-Origin-Embedder-Policy} header + * @param embedderPolicy the {@link CrossOriginEmbedderPolicy} to use + */ + public void setPolicy(CrossOriginEmbedderPolicy embedderPolicy) { + Assert.notNull(embedderPolicy, "embedderPolicy cannot be null"); + this.delegate = createDelegate(embedderPolicy); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(CrossOriginEmbedderPolicy embedderPolicy) { + StaticServerHttpHeadersWriter.Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(EMBEDDER_POLICY, embedderPolicy.getPolicy()); + return builder.build(); + } + + public enum CrossOriginEmbedderPolicy { + + UNSAFE_NONE("unsafe-none"), + + REQUIRE_CORP("require-corp"); + + private final String policy; + + CrossOriginEmbedderPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java new file mode 100644 index 00000000000..d02add2320e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Inserts Cross-Origin-Opener-Policy header. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Opener-Policy + */ +public final class CrossOriginOpenerPolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String OPENER_POLICY = "Cross-Origin-Opener-Policy"; + + private ServerHttpHeadersWriter delegate; + + /** + * Sets the {@link CrossOriginOpenerPolicy} value to be used in the + * {@code Cross-Origin-Opener-Policy} header + * @param openerPolicy the {@link CrossOriginOpenerPolicy} to use + */ + public void setPolicy(CrossOriginOpenerPolicy openerPolicy) { + Assert.notNull(openerPolicy, "openerPolicy cannot be null"); + this.delegate = createDelegate(openerPolicy); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(CrossOriginOpenerPolicy openerPolicy) { + StaticServerHttpHeadersWriter.Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(OPENER_POLICY, openerPolicy.getPolicy()); + return builder.build(); + } + + public enum CrossOriginOpenerPolicy { + + UNSAFE_NONE("unsafe-none"), + + SAME_ORIGIN_ALLOW_POPUPS("same-origin-allow-popups"), + + SAME_ORIGIN("same-origin"); + + private final String policy; + + CrossOriginOpenerPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java new file mode 100644 index 00000000000..dff25749441 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Inserts Cross-Origin-Resource-Policy headers. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Resource-Policy + */ +public final class CrossOriginResourcePolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String RESOURCE_POLICY = "Cross-Origin-Resource-Policy"; + + private ServerHttpHeadersWriter delegate; + + /** + * Sets the {@link CrossOriginResourcePolicy} value to be used in the + * {@code Cross-Origin-Embedder-Policy} header + * @param resourcePolicy the {@link CrossOriginResourcePolicy} to use + */ + public void setPolicy(CrossOriginResourcePolicy resourcePolicy) { + Assert.notNull(resourcePolicy, "resourcePolicy cannot be null"); + this.delegate = createDelegate(resourcePolicy); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(CrossOriginResourcePolicy resourcePolicy) { + StaticServerHttpHeadersWriter.Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(RESOURCE_POLICY, resourcePolicy.getPolicy()); + return builder.build(); + } + + public enum CrossOriginResourcePolicy { + + SAME_SITE("same-site"), + + SAME_ORIGIN("same-origin"), + + CROSS_ORIGIN("cross-origin"); + + private final String policy; + + CrossOriginResourcePolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java new file mode 100644 index 00000000000..6c4b008b5c8 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Delegates to a provided {@link ServerHttpHeadersWriter} if + * {@link ServerWebExchangeMatcher#matches(ServerWebExchange)} returns a match. + * + * @author David Herberth + * @since 5.8 + */ +public final class ServerWebExchangeDelegatingServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + private final ServerWebExchangeMatcherEntry headersWriter; + + /** + * Creates a new instance + * @param headersWriter the {@link ServerWebExchangeMatcherEntry} holding a + * {@link ServerWebExchangeMatcher} and the {@link ServerHttpHeadersWriter} to invoke + * if the matcher returns a match. + */ + public ServerWebExchangeDelegatingServerHttpHeadersWriter( + ServerWebExchangeMatcherEntry headersWriter) { + Assert.notNull(headersWriter, "headersWriter cannot be null"); + Assert.notNull(headersWriter.getMatcher(), "webExchangeMatcher cannot be null"); + Assert.notNull(headersWriter.getEntry(), "delegateHeadersWriter cannot be null"); + this.headersWriter = headersWriter; + } + + /** + * Creates a new instance + * @param webExchangeMatcher the {@link ServerWebExchangeMatcher} to use. If it + * returns a match, the delegateHeadersWriter is invoked. + * @param delegateHeadersWriter the {@link ServerHttpHeadersWriter} to invoke if the + * {@link ServerWebExchangeMatcher} returns a match. + */ + public ServerWebExchangeDelegatingServerHttpHeadersWriter(ServerWebExchangeMatcher webExchangeMatcher, + ServerHttpHeadersWriter delegateHeadersWriter) { + this(new ServerWebExchangeMatcherEntry<>(webExchangeMatcher, delegateHeadersWriter)); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return this.headersWriter.getMatcher().matches(exchange).filter(ServerWebExchangeMatcher.MatchResult::isMatch) + .flatMap((matchResult) -> this.headersWriter.getEntry().writeHttpHeaders(exchange)); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java index 1f636f5cd33..fb3d3c4d774 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java @@ -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. @@ -17,7 +17,6 @@ package org.springframework.security.web.server.header; import java.util.Arrays; -import java.util.Collections; import reactor.core.publisher.Mono; @@ -41,8 +40,16 @@ public StaticServerHttpHeadersWriter(HttpHeaders headersToAdd) { @Override public Mono writeHttpHeaders(ServerWebExchange exchange) { HttpHeaders headers = exchange.getResponse().getHeaders(); - boolean containsOneHeaderToAdd = Collections.disjoint(headers.keySet(), this.headersToAdd.keySet()); - if (containsOneHeaderToAdd) { + // Note: We need to ensure that the following algorithm compares headers + // case insensitively, which should be true of headers.containsKey(). + boolean containsNoHeadersToAdd = true; + for (String headerName : this.headersToAdd.keySet()) { + if (headers.containsKey(headerName)) { + containsNoHeadersToAdd = false; + break; + } + } + if (containsNoHeadersToAdd) { this.headersToAdd.forEach(headers::put); } return Mono.empty(); diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java new file mode 100644 index 00000000000..5bf439a5e02 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.util.matcher; + +import reactor.core.publisher.Mono; + +import org.springframework.security.web.util.matcher.IpAddressMatcher; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Matches a request based on IP Address or subnet mask matching against the remote + * address. + * + * @author Guirong Hu + * @since 5.7 + */ +public final class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher { + + private final IpAddressMatcher ipAddressMatcher; + + /** + * Takes a specific IP address or a range specified using the IP/Netmask (e.g. + * 192.168.1.0/24 or 202.24.0.0/14). + * @param ipAddress the address or range of addresses from which the request must + * come. + */ + public IpAddressServerWebExchangeMatcher(String ipAddress) { + Assert.hasText(ipAddress, "IP address cannot be empty"); + this.ipAddressMatcher = new IpAddressMatcher(ipAddress); + } + + @Override + public Mono matches(ServerWebExchange exchange) { + // @formatter:off + return Mono.justOrEmpty(exchange.getRequest().getRemoteAddress()) + .map((remoteAddress) -> remoteAddress.getAddress().getHostAddress()) + .map(this.ipAddressMatcher::matches) + .flatMap((matches) -> matches ? MatchResult.match() : MatchResult.notMatch()) + .switchIfEmpty(MatchResult.notMatch()); + // @formatter:on + } + + @Override + public String toString() { + return "IpAddressServerWebExchangeMatcher{ipAddressMatcher=" + this.ipAddressMatcher + '}'; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java index 3ddc6b09df3..654940be5d9 100644 --- a/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -66,7 +66,7 @@ public MediaTypeServerWebExchangeMatcher(MediaType... matchingMediaTypes) { */ public MediaTypeServerWebExchangeMatcher(Collection matchingMediaTypes) { Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null"); - Assert.isTrue(!matchingMediaTypes.contains(null), + Assert.noNullElements(matchingMediaTypes, () -> "matchingMediaTypes cannot contain null. Got " + matchingMediaTypes); this.matchingMediaTypes = matchingMediaTypes; } diff --git a/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java b/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java index 51113ac551a..57853a39b84 100644 --- a/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java +++ b/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -32,6 +32,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; @@ -42,6 +43,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -79,6 +81,8 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationManager authenticationManager; @@ -233,7 +237,11 @@ public void login(String username, String password) throws ServletException { private Authentication getAuthentication(AuthenticationManager authManager, String username, String password) throws ServletException { try { - return authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken + .unauthenticated(username, password); + Object details = HttpServlet3RequestFactory.this.authenticationDetailsSource.buildDetails(this); + authentication.setDetails(details); + return authManager.authenticate(authentication); } catch (AuthenticationException ex) { SecurityContextHolder.clearContext(); diff --git a/web/src/main/java/org/springframework/security/web/session/DisableEncodeUrlFilter.java b/web/src/main/java/org/springframework/security/web/session/DisableEncodeUrlFilter.java new file mode 100644 index 00000000000..e09325c0957 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/session/DisableEncodeUrlFilter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.session; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Disables encoding URLs using the {@link HttpServletResponse} to prevent including the + * session id in URLs which is not considered URL because the session id can be leaked in + * things like HTTP access logs. + * + * @author Rob Winch + * @since 5.7 + */ +public class DisableEncodeUrlFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response)); + } + + /** + * Disables URL rewriting for the {@link HttpServletResponse} to prevent including the + * session id in URLs which is not considered URL because the session id can be leaked + * in things like HTTP access logs. + * + * @author Rob Winch + * @since 5.7 + */ + private static final class DisableEncodeUrlResponseWrapper extends HttpServletResponseWrapper { + + /** + * Constructs a response adaptor wrapping the given response. + * @param response the {@link HttpServletResponse} to be wrapped. + * @throws IllegalArgumentException if the response is null + */ + private DisableEncodeUrlResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public String encodeRedirectUrl(String url) { + return url; + } + + @Override + public String encodeRedirectURL(String url) { + return url; + } + + @Override + public String encodeUrl(String url) { + return url; + } + + @Override + public String encodeURL(String url) { + return url; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/session/ForceEagerSessionCreationFilter.java b/web/src/main/java/org/springframework/security/web/session/ForceEagerSessionCreationFilter.java new file mode 100644 index 00000000000..0164de4b6cd --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/session/ForceEagerSessionCreationFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.session; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.core.log.LogMessage; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Eagerly creates {@link HttpSession} if it does not already exist. + * + * @author Rob Winch + * @since 5.7 + */ +public class ForceEagerSessionCreationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + HttpSession session = request.getSession(); + if (this.logger.isDebugEnabled() && session.isNew()) { + this.logger.debug(LogMessage.format("Created session eagerly")); + } + filterChain.doFilter(request, response); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java index 07682a183f5..765c5404932 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -45,7 +45,7 @@ public final class AndRequestMatcher implements RequestMatcher { */ public AndRequestMatcher(List requestMatchers) { Assert.notEmpty(requestMatchers, "requestMatchers must contain a value"); - Assert.isTrue(!requestMatchers.contains(null), "requestMatchers cannot contain null values"); + Assert.noNullElements(requestMatchers, "requestMatchers cannot contain null values"); this.requestMatchers = requestMatchers; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java index 49aed044786..6d9c226b57c 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java @@ -49,6 +49,7 @@ * @author Rob Winch * @author Eddú Meléndez * @author Evgeniy Cheban + * @author Manuel Jordan * @since 3.1 * @see org.springframework.util.AntPathMatcher */ diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java index f3ce9d70a60..492ea1eda26 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java @@ -29,7 +29,7 @@ * *

      * With the default EvaluationContext ({@link ELRequestMatcherContext}) you can use - * hasIpAdress() and hasHeader() + * hasIpAddress() and hasHeader() *

      * *

      diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java index 92a2e2eb784..ae7dbaaaa5f 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -40,7 +40,7 @@ public final class OrRequestMatcher implements RequestMatcher { */ public OrRequestMatcher(List requestMatchers) { Assert.notEmpty(requestMatchers, "requestMatchers must contain a value"); - Assert.isTrue(!requestMatchers.contains(null), "requestMatchers cannot contain null values"); + Assert.noNullElements(requestMatchers, "requestMatchers cannot contain null values"); this.requestMatchers = requestMatchers; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java index 9264b56f213..a334afc736a 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java @@ -43,7 +43,9 @@ */ public final class RegexRequestMatcher implements RequestMatcher { - private static final int DEFAULT = 0; + private static final int DEFAULT = Pattern.DOTALL; + + private static final int CASE_INSENSITIVE = DEFAULT | Pattern.CASE_INSENSITIVE; private static final Log logger = LogFactory.getLog(RegexRequestMatcher.class); @@ -68,7 +70,7 @@ public RegexRequestMatcher(String pattern, String httpMethod) { * {@link Pattern#CASE_INSENSITIVE} flag set. */ public RegexRequestMatcher(String pattern, String httpMethod, boolean caseInsensitive) { - this.pattern = Pattern.compile(pattern, caseInsensitive ? Pattern.CASE_INSENSITIVE : DEFAULT); + this.pattern = Pattern.compile(pattern, caseInsensitive ? CASE_INSENSITIVE : DEFAULT); this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java new file mode 100644 index 00000000000..0cad6e3f084 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.util.matcher; + +/** + * A rich object for associating a {@link RequestMatcher} to another object. + * + * @author Marcus Da Coregio + * @since 5.5.5 + */ +public class RequestMatcherEntry { + + private final RequestMatcher requestMatcher; + + private final T entry; + + public RequestMatcherEntry(RequestMatcher requestMatcher, T entry) { + this.requestMatcher = requestMatcher; + this.entry = entry; + } + + public RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + + public T getEntry() { + return this.entry; + } + +} diff --git a/web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java b/web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java new file mode 100644 index 00000000000..e05ca3b6d83 --- /dev/null +++ b/web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.test.web; + +import java.util.Base64; + +import org.springframework.util.DigestUtils; + +public final class CodecTestUtils { + + private CodecTestUtils() { + } + + public static String encodeBase64(String unencoded) { + return Base64.getEncoder().encodeToString(unencoded.getBytes()); + } + + public static String encodeBase64(byte[] unencoded) { + return Base64.getEncoder().encodeToString(unencoded); + } + + public static String decodeBase64(String encoded) { + return new String(Base64.getDecoder().decode(encoded)); + } + + public static boolean isBase64(byte[] arrayOctet) { + try { + Base64.getMimeDecoder().decode(arrayOctet); + return true; + + } + catch (Exception ex) { + return false; + } + } + + public static String md5Hex(String data) { + return DigestUtils.md5DigestAsHex(data.getBytes()); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 00000000000..39d9e068f43 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class AuthorizationManagerWebInvocationPrivilegeEvaluatorTests { + + @InjectMocks + private AuthorizationManagerWebInvocationPrivilegeEvaluator privilegeEvaluator; + + @Mock + private AuthorizationManager authorizationManager; + + @Test + void constructorWhenAuthorizationManagerNullThenIllegalArgument() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerWebInvocationPrivilegeEvaluator(null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + void isAllowedWhenAuthorizationManagerAllowsThenAllowedTrue() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true)); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isTrue(); + verify(this.authorizationManager).check(any(), any()); + } + + @Test + void isAllowedWhenAuthorizationManagerDeniesAllowedFalse() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isFalse(); + } + + @Test + void isAllowedWhenAuthorizationManagerAbstainsThenAllowedTrue() { + given(this.authorizationManager.check(any(), any())).willReturn(null); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isTrue(); + } + + @Test + void isAllowedWhenServletContextExistsThenFilterInvocationHasServletContext() { + ServletContext servletContext = new MockServletContext(); + this.privilegeEvaluator.setServletContext(servletContext); + this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpServletRequest.class); + verify(this.authorizationManager).check(any(), captor.capture()); + assertThat(captor.getValue().getServletContext()).isSameAs(servletContext); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 00000000000..dd561ea7bba --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link RequestMatcherDelegatingWebInvocationPrivilegeEvaluator} + * + * @author Marcus Da Coregio + */ +class RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests { + + private final RequestMatcher alwaysMatch = mock(RequestMatcher.class); + + private final RequestMatcher alwaysDeny = mock(RequestMatcher.class); + + private final String uri = "/test"; + + private final Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + @BeforeEach + void setup() { + given(this.alwaysMatch.matches(any())).willReturn(true); + given(this.alwaysDeny.matches(any())).willReturn(false); + } + + @Test + void isAllowedWhenDelegatesEmptyThenAllowed() { + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.emptyList()); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenNotMatchThenAllowed() { + RequestMatcherEntry> notMatch = new RequestMatcherEntry<>(this.alwaysDeny, + Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(notMatch)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + verify(notMatch.getRequestMatcher()).matches(any()); + } + + @Test + void isAllowedWhenPrivilegeEvaluatorAllowThenAllowedTrue() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenPrivilegeEvaluatorDenyThenAllowedFalse() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysDeny())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + } + + @Test + void isAllowedWhenNotMatchThenMatchThenOnlySecondDelegateInvoked() { + RequestMatcherEntry> notMatchDelegate = new RequestMatcherEntry<>( + this.alwaysDeny, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> matchDelegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> spyNotMatchDelegate = spy(notMatchDelegate); + RequestMatcherEntry> spyMatchDelegate = spy(matchDelegate); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Arrays.asList(notMatchDelegate, spyMatchDelegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + verify(spyNotMatchDelegate.getRequestMatcher()).matches(any()); + verify(spyNotMatchDelegate, never()).getEntry(); + verify(spyMatchDelegate.getRequestMatcher()).matches(any()); + verify(spyMatchDelegate, times(2)).getEntry(); // 2 times, one for constructor and + // other one in isAllowed + } + + @Test + void isAllowedWhenDelegatePrivilegeEvaluatorsEmptyThenAllowedTrue() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.emptyList()); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenFirstDelegateDenyThenDoNotInvokeOthers() { + WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny(); + WebInvocationPrivilegeEvaluator allow = TestWebInvocationPrivilegeEvaluator.alwaysAllow(); + WebInvocationPrivilegeEvaluator spyDeny = spy(deny); + WebInvocationPrivilegeEvaluator spyAllow = spy(allow); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Arrays.asList(spyDeny, spyAllow)); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + verify(spyDeny).isAllowed(any(), any()); + verifyNoInteractions(spyAllow); + } + + @Test + void isAllowedWhenDifferentArgumentsThenCallSpecificIsAllowedInDelegate() { + WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny(); + WebInvocationPrivilegeEvaluator spyDeny = spy(deny); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(spyDeny)); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + assertThat(delegating.isAllowed("/cp", this.uri, "GET", this.authentication)).isFalse(); + verify(spyDeny).isAllowed(any(), any()); + verify(spyDeny).isAllowed(any(), any(), any(), any()); + verifyNoMoreInteractions(spyDeny); + } + + @Test + void isAllowedWhenServletContextIsSetThenPassedFilterInvocationHttpServletRequestHasServletContext() { + Authentication token = new TestingAuthenticationToken("test", "Password", "MOCK_INDEX"); + MockServletContext servletContext = new MockServletContext(); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpServletRequest.class); + RequestMatcher requestMatcher = mock(RequestMatcher.class); + WebInvocationPrivilegeEvaluator wipe = mock(WebInvocationPrivilegeEvaluator.class); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>(requestMatcher, + Collections.singletonList(wipe)); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator requestMatcherWipe = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + requestMatcherWipe.setServletContext(servletContext); + requestMatcherWipe.isAllowed("/foo/index.jsp", token); + verify(requestMatcher).matches(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().getServletContext()).isNotNull(); + } + + @Test + void constructorWhenPrivilegeEvaluatorsNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(this.alwaysMatch, + null); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("webInvocationPrivilegeEvaluators cannot be null"); + } + + @Test + void constructorWhenRequestMatcherNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(null, + Collections.singletonList(mock(WebInvocationPrivilegeEvaluator.class))); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("requestMatcher cannot be null"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java new file mode 100644 index 00000000000..54ab666cd52 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import org.springframework.security.core.Authentication; + +public final class TestWebInvocationPrivilegeEvaluator { + + private static final AlwaysAllowWebInvocationPrivilegeEvaluator ALWAYS_ALLOW = new AlwaysAllowWebInvocationPrivilegeEvaluator(); + + private static final AlwaysDenyWebInvocationPrivilegeEvaluator ALWAYS_DENY = new AlwaysDenyWebInvocationPrivilegeEvaluator(); + + private TestWebInvocationPrivilegeEvaluator() { + } + + public static WebInvocationPrivilegeEvaluator alwaysAllow() { + return ALWAYS_ALLOW; + } + + public static WebInvocationPrivilegeEvaluator alwaysDeny() { + return ALWAYS_DENY; + } + + private static class AlwaysAllowWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return true; + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + return true; + } + + } + + private static class AlwaysDenyWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return false; + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + return false; + } + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandlerTests.java b/web/src/test/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandlerTests.java new file mode 100644 index 00000000000..5c58c7fe902 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandlerTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access.expression; + +import java.util.function.Supplier; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.TypedValue; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class DefaultHttpSecurityExpressionHandlerTests { + + @Mock + private AuthenticationTrustResolver trustResolver; + + @Mock + private Authentication authentication; + + @Mock + private RequestAuthorizationContext context; + + private DefaultHttpSecurityExpressionHandler handler; + + @BeforeEach + public void setup() { + this.handler = new DefaultHttpSecurityExpressionHandler(); + } + + @AfterEach + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void expressionPropertiesAreResolvedAgainstAppContextBeans() { + StaticApplicationContext appContext = new StaticApplicationContext(); + RootBeanDefinition bean = new RootBeanDefinition(SecurityConfig.class); + bean.getConstructorArgumentValues().addGenericArgumentValue("ROLE_A"); + appContext.registerBeanDefinition("role", bean); + this.handler.setApplicationContext(appContext); + EvaluationContext ctx = this.handler.createEvaluationContext(mock(Authentication.class), + mock(RequestAuthorizationContext.class)); + ExpressionParser parser = this.handler.getExpressionParser(); + assertThat(parser.parseExpression("@role.getAttribute() == 'ROLE_A'").getValue(ctx, Boolean.class)).isTrue(); + assertThat(parser.parseExpression("@role.attribute == 'ROLE_A'").getValue(ctx, Boolean.class)).isTrue(); + } + + @Test + public void setTrustResolverNull() { + assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setTrustResolver(null)); + } + + @Test + public void createEvaluationContextCustomTrustResolver() { + this.handler.setTrustResolver(this.trustResolver); + Expression expression = this.handler.getExpressionParser().parseExpression("anonymous"); + EvaluationContext context = this.handler.createEvaluationContext(this.authentication, this.context); + assertThat(expression.getValue(context, Boolean.class)).isFalse(); + verify(this.trustResolver).isAnonymous(this.authentication); + } + + @Test + public void createEvaluationContextSupplierAuthentication() { + Supplier mockAuthenticationSupplier = mock(Supplier.class); + given(mockAuthenticationSupplier.get()).willReturn(this.authentication); + EvaluationContext context = this.handler.createEvaluationContext(mockAuthenticationSupplier, this.context); + verifyNoInteractions(mockAuthenticationSupplier); + assertThat(context.getRootObject()).extracting(TypedValue::getValue) + .asInstanceOf(InstanceOfAssertFactories.type(WebSecurityExpressionRoot.class)) + .extracting(SecurityExpressionRoot::getAuthentication).isEqualTo(this.authentication); + verify(mockAuthenticationSupplier).get(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java new file mode 100644 index 00000000000..c10ae293ef4 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access.expression; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link WebExpressionAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +class WebExpressionAuthorizationManagerTests { + + @Test + void instantiateWhenExpressionStringNullThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new WebExpressionAuthorizationManager(null)) + .withMessage("expressionString cannot be empty"); + } + + @Test + void instantiateWhenExpressionStringEmptyThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new WebExpressionAuthorizationManager("")) + .withMessage("expressionString cannot be empty"); + } + + @Test + void instantiateWhenExpressionStringBlankThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new WebExpressionAuthorizationManager(" ")) + .withMessage("expressionString cannot be empty"); + } + + @Test + void instantiateWhenExpressionHandlerNotSetThenDefaultUsed() { + WebExpressionAuthorizationManager manager = new WebExpressionAuthorizationManager("hasRole('ADMIN')"); + assertThat(manager).extracting("expressionHandler").isInstanceOf(DefaultHttpSecurityExpressionHandler.class); + } + + @Test + void setExpressionHandlerWhenNullThenIllegalArgumentException() { + WebExpressionAuthorizationManager manager = new WebExpressionAuthorizationManager("hasRole('ADMIN')"); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + void setExpressionHandlerWhenNotNullThenVerifyExpressionHandler() { + String expressionString = "hasRole('ADMIN')"; + WebExpressionAuthorizationManager manager = new WebExpressionAuthorizationManager(expressionString); + DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler(); + ExpressionParser mockExpressionParser = mock(ExpressionParser.class); + Expression mockExpression = mock(Expression.class); + given(mockExpressionParser.parseExpression(expressionString)).willReturn(mockExpression); + expressionHandler.setExpressionParser(mockExpressionParser); + manager.setExpressionHandler(expressionHandler); + assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler); + assertThat(manager).extracting("expression").isEqualTo(mockExpression); + verify(mockExpressionParser).parseExpression(expressionString); + } + + @Test + void checkWhenExpressionHasRoleAdminConfiguredAndRoleAdminThenGrantedDecision() { + WebExpressionAuthorizationManager manager = new WebExpressionAuthorizationManager("hasRole('ADMIN')"); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, + new RequestAuthorizationContext(new MockHttpServletRequest())); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + void checkWhenExpressionHasRoleAdminConfiguredAndRoleUserThenDeniedDecision() { + WebExpressionAuthorizationManager manager = new WebExpressionAuthorizationManager("hasRole('ADMIN')"); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + new RequestAuthorizationContext(new MockHttpServletRequest())); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java index b2b22c5666d..e0d885fee14 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java @@ -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,6 +18,7 @@ import java.util.function.Supplier; +import javax.servlet.DispatcherType; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; @@ -31,16 +32,21 @@ import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -61,6 +67,8 @@ public void tearDown() { @Test public void filterWhenAuthorizationManagerVerifyPassesThenNextFilter() throws Exception { AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class); + given(mockAuthorizationManager.check(any(Supplier.class), any(HttpServletRequest.class))) + .willReturn(new AuthorizationDecision(true)); AuthorizationFilter filter = new AuthorizationFilter(mockAuthorizationManager); TestingAuthenticationToken authenticationToken = new TestingAuthenticationToken("user", "password"); @@ -75,7 +83,7 @@ public void filterWhenAuthorizationManagerVerifyPassesThenNextFilter() throws Ex filter.doFilter(mockRequest, mockResponse, mockFilterChain); ArgumentCaptor> authenticationCaptor = ArgumentCaptor.forClass(Supplier.class); - verify(mockAuthorizationManager).verify(authenticationCaptor.capture(), eq(mockRequest)); + verify(mockAuthorizationManager).check(authenticationCaptor.capture(), eq(mockRequest)); Supplier authentication = authenticationCaptor.getValue(); assertThat(authentication.get()).isEqualTo(authenticationToken); @@ -96,7 +104,7 @@ public void filterWhenAuthorizationManagerVerifyThrowsAccessDeniedExceptionThenS MockHttpServletResponse mockResponse = new MockHttpServletResponse(); FilterChain mockFilterChain = mock(FilterChain.class); - willThrow(new AccessDeniedException("Access Denied")).given(mockAuthorizationManager).verify(any(), + willThrow(new AccessDeniedException("Access Denied")).given(mockAuthorizationManager).check(any(), eq(mockRequest)); assertThatExceptionOfType(AccessDeniedException.class) @@ -104,7 +112,7 @@ public void filterWhenAuthorizationManagerVerifyThrowsAccessDeniedExceptionThenS .withMessage("Access Denied"); ArgumentCaptor> authenticationCaptor = ArgumentCaptor.forClass(Supplier.class); - verify(mockAuthorizationManager).verify(authenticationCaptor.capture(), eq(mockRequest)); + verify(mockAuthorizationManager).check(authenticationCaptor.capture(), eq(mockRequest)); Supplier authentication = authenticationCaptor.getValue(); assertThat(authentication.get()).isEqualTo(authenticationToken); @@ -125,4 +133,101 @@ public void filterWhenAuthenticationNullThenAuthenticationCredentialsNotFoundExc verifyNoInteractions(mockFilterChain); } + @Test + public void getAuthorizationManager() { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + assertThat(authorizationFilter.getAuthorizationManager()).isSameAs(authorizationManager); + } + + @Test + public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + assertThatIllegalArgumentException().isThrownBy(() -> authorizationFilter.setAuthorizationEventPublisher(null)) + .withMessage("eventPublisher cannot be null"); + } + + @Test + public void doFilterWhenAuthorizationEventPublisherThenUses() throws Exception { + AuthorizationFilter authorizationFilter = new AuthorizationFilter( + AuthenticatedAuthorizationManager.authenticated()); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + SecurityContextHolder.setContext(securityContext); + + AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class); + authorizationFilter.setAuthorizationEventPublisher(eventPublisher); + authorizationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(HttpServletRequest.class), + any(AuthorizationDecision.class)); + } + + @Test + public void doFilterWhenErrorThenDoNotFilter() throws Exception { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + mockRequest.setDispatcherType(DispatcherType.ERROR); + mockRequest.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + authorizationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + verifyNoInteractions(authorizationManager); + } + + @Test + public void doFilterWhenErrorAndShouldFilterAllDispatcherTypesThenFilter() throws Exception { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + authorizationFilter.setShouldFilterAllDispatcherTypes(true); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + mockRequest.setDispatcherType(DispatcherType.ERROR); + mockRequest.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + authorizationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + verify(authorizationManager).check(any(Supplier.class), any(HttpServletRequest.class)); + } + + @Test + public void doFilterNestedErrorDispatchWhenAuthorizationManagerThenUses() throws Exception { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + authorizationFilter.setShouldFilterAllDispatcherTypes(true); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + mockRequest.setDispatcherType(DispatcherType.ERROR); + mockRequest.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + authorizationFilter.doFilterNestedErrorDispatch(mockRequest, mockResponse, mockFilterChain); + verify(authorizationManager).check(any(Supplier.class), any(HttpServletRequest.class)); + } + + @Test + public void doFilterNestedErrorDispatchWhenAuthorizationEventPublisherThenUses() throws Exception { + AuthorizationFilter authorizationFilter = new AuthorizationFilter( + AuthenticatedAuthorizationManager.authenticated()); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + SecurityContextHolder.setContext(securityContext); + + AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class); + authorizationFilter.setAuthorizationEventPublisher(eventPublisher); + authorizationFilter.doFilterNestedErrorDispatch(mockRequest, mockResponse, mockFilterChain); + verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(HttpServletRequest.class), + any(AuthorizationDecision.class)); + } + } diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java index 829866340a1..624a6ecee8a 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java @@ -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. @@ -22,9 +22,12 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.core.Authentication; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -33,6 +36,7 @@ * Tests for {@link RequestMatcherDelegatingAuthorizationManager}. * * @author Evgeniy Cheban + * @author Parikshit Dutta */ public class RequestMatcherDelegatingAuthorizationManagerTests { @@ -83,4 +87,39 @@ public void checkWhenMultipleMappingsConfiguredThenDelegatesMatchingManager() { assertThat(abstain).isNull(); } + @Test + public void checkWhenMultipleMappingsConfiguredWithConsumerThenDelegatesMatchingManager() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .mappings((m) -> { + m.add(new RequestMatcherEntry<>(new MvcRequestMatcher(null, "/grant"), + (a, o) -> new AuthorizationDecision(true))); + m.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, + AuthorityAuthorizationManager.hasRole("ADMIN"))); + m.add(new RequestMatcherEntry<>(new MvcRequestMatcher(null, "/afterAny"), + (a, o) -> new AuthorizationDecision(true))); + }).build(); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/grant")); + + assertThat(grant).isNotNull(); + assertThat(grant.isGranted()).isTrue(); + + AuthorizationDecision afterAny = manager.check(authentication, new MockHttpServletRequest(null, "/afterAny")); + assertThat(afterAny).isNotNull(); + assertThat(afterAny.isGranted()).isFalse(); + + AuthorizationDecision unmapped = manager.check(authentication, new MockHttpServletRequest(null, "/unmapped")); + assertThat(unmapped).isNotNull(); + assertThat(unmapped.isGranted()).isFalse(); + } + + @Test + public void addWhenMappingsConsumerNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder().mappings(null).build()) + .withMessage("mappingsConsumer cannot be null"); + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java index c41c12eab80..a2b9a78898d 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.mock.web.MockFilterConfig; import org.springframework.mock.web.MockHttpServletRequest; @@ -34,14 +35,17 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServicesTests; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.firewall.DefaultHttpFirewall; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -322,6 +326,37 @@ public void testSuccessfulAuthenticationInvokesSuccessHandlerAndSetsContext() th assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); } + @Test + public void testSuccessfulAuthenticationThenDefaultDoesNotCreateSession() throws Exception { + Authentication authentication = TestAuthentication.authenticatedUser(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(false); + MockAuthenticationFilter filter = new MockAuthenticationFilter(); + + filter.successfulAuthentication(request, response, chain, authentication); + + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void testSuccessfulAuthenticationWhenCustomSecurityContextRepositoryThenAuthenticationSaved() + throws Exception { + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(SecurityContext.class); + SecurityContextRepository repository = mock(SecurityContextRepository.class); + Authentication authentication = TestAuthentication.authenticatedUser(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(false); + MockAuthenticationFilter filter = new MockAuthenticationFilter(); + filter.setSecurityContextRepository(repository); + + filter.successfulAuthentication(request, response, chain, authentication); + + verify(repository).saveContext(contextCaptor.capture(), eq(request), eq(response)); + assertThat(contextCaptor.getValue().getAuthentication()).isEqualTo(authentication); + } + @Test public void testFailedAuthenticationInvokesFailureHandler() throws Exception { // Setup our HTTP request @@ -440,7 +475,7 @@ private MockAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMat public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.grantAccess) { - return new UsernamePasswordAuthenticationToken("test", "test", + return UsernamePasswordAuthenticationToken.authenticated("test", "test", AuthorityUtils.createAuthorityList("TEST")); } else { diff --git a/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java index a8a97eebb10..4038c65ff60 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -38,7 +39,9 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.RequestMatcher; import static org.assertj.core.api.Assertions.assertThat; @@ -256,4 +259,36 @@ public void filterWhenSuccessfulAuthenticationThenSessionIdChanges() throws Exce assertThat(session.getId()).isNotEqualTo(sessionId); } + @Test + public void filterWhenSuccessfulAuthenticationThenNoSessionCreated() throws Exception { + Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE_USER"); + given(this.authenticationConverter.convert(any())).willReturn(authentication); + given(this.authenticationManager.authenticate(any())).willReturn(authentication); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = new MockFilterChain(); + AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager, + this.authenticationConverter); + filter.doFilter(request, response, chain); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void filterWhenCustomSecurityContextRepositoryAndSuccessfulAuthenticationRepositoryUsed() throws Exception { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + ArgumentCaptor securityContextArg = ArgumentCaptor.forClass(SecurityContext.class); + Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE_USER"); + given(this.authenticationConverter.convert(any())).willReturn(authentication); + given(this.authenticationManager.authenticate(any())).willReturn(authentication); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = new MockFilterChain(); + AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager, + this.authenticationConverter); + filter.setSecurityContextRepository(securityContextRepository); + filter.doFilter(request, response, chain); + verify(securityContextRepository).saveContext(securityContextArg.capture(), eq(request), eq(response)); + assertThat(securityContextArg.getValue().getAuthentication()).isEqualTo(authentication); + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java b/web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java new file mode 100644 index 00000000000..df0f7258f42 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RequestMatcherDelegatingAuthenticationManagerResolverTests} + * + * @author Josh Cummings + */ +public class RequestMatcherDelegatingAuthenticationManagerResolverTests { + + private AuthenticationManager one = mock(AuthenticationManager.class); + + private AuthenticationManager two = mock(AuthenticationManager.class); + + @Test + public void resolveWhenMatchesThenReturnsAuthenticationManager() { + RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver + .builder().add(new AntPathRequestMatcher("/one/**"), this.one) + .add(new AntPathRequestMatcher("/two/**"), this.two).build(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/one/location"); + request.setServletPath("/one/location"); + assertThat(resolver.resolve(request)).isEqualTo(this.one); + } + + @Test + public void resolveWhenDoesNotMatchThenReturnsDefaultAuthenticationManager() { + RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver + .builder().add(new AntPathRequestMatcher("/one/**"), this.one) + .add(new AntPathRequestMatcher("/two/**"), this.two).build(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/wrong/location"); + AuthenticationManager authenticationManager = resolver.resolve(request); + + Authentication authentication = new TestingAuthenticationToken("principal", "creds"); + assertThatExceptionOfType(AuthenticationServiceException.class) + .isThrownBy(() -> authenticationManager.authenticate(authentication)); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java index d32323c4e72..562a35f6526 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.springframework.mock.web.MockFilterChain; @@ -34,17 +35,20 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler; import org.springframework.security.web.authentication.ForwardAuthenticationSuccessHandler; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -210,6 +214,31 @@ public void callsAuthenticationSuccessHandlerOnSuccessfulAuthentication() throws assertThat(response.getForwardedUrl()).isEqualTo("/forwardUrl"); } + @Test + public void securityContextRepository() throws Exception { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + Object currentPrincipal = "currentUser"; + TestingAuthenticationToken authRequest = new TestingAuthenticationToken(currentPrincipal, "something", + "ROLE_USER"); + SecurityContextHolder.getContext().setAuthentication(authRequest); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + ConcretePreAuthenticatedProcessingFilter filter = new ConcretePreAuthenticatedProcessingFilter(); + filter.setSecurityContextRepository(securityContextRepository); + filter.setAuthenticationSuccessHandler(new ForwardAuthenticationSuccessHandler("/forwardUrl")); + filter.setCheckForPrincipalChanges(true); + filter.principal = "newUser"; + AuthenticationManager am = mock(AuthenticationManager.class); + given(am.authenticate(any())).willReturn(authRequest); + filter.setAuthenticationManager(am); + filter.afterPropertiesSet(); + filter.doFilter(request, response, chain); + ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); + verify(securityContextRepository).saveContext(contextArg.capture(), eq(request), eq(response)); + assertThat(contextArg.getValue().getAuthentication().getPrincipal()).isEqualTo(authRequest.getName()); + } + @Test public void callsAuthenticationFailureHandlerOnFailedAuthentication() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -251,8 +280,8 @@ public void requiresAuthenticationFalsePrincipalNotString() throws Exception { @Test public void requiresAuthenticationFalsePrincipalUser() throws Exception { User currentPrincipal = new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER")); - UsernamePasswordAuthenticationToken currentAuthentication = new UsernamePasswordAuthenticationToken( - currentPrincipal, currentPrincipal.getPassword(), currentPrincipal.getAuthorities()); + UsernamePasswordAuthenticationToken currentAuthentication = UsernamePasswordAuthenticationToken + .authenticated(currentPrincipal, currentPrincipal.getPassword(), currentPrincipal.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(currentAuthentication); MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProviderTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProviderTests.java index 671385b8f8e..b278c8c2ce2 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProviderTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -46,7 +46,7 @@ public final void afterPropertiesSet() { public final void authenticateInvalidToken() throws Exception { UserDetails ud = new User("dummyUser", "dummyPwd", true, true, true, true, AuthorityUtils.NO_AUTHORITIES); PreAuthenticatedAuthenticationProvider provider = getProvider(ud); - Authentication request = new UsernamePasswordAuthenticationToken("dummyUser", "dummyPwd"); + Authentication request = UsernamePasswordAuthenticationToken.unauthenticated("dummyUser", "dummyPwd"); Authentication result = provider.authenticate(request); assertThat(result).isNull(); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java index a8912f326a3..d788ca654cf 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java @@ -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. @@ -287,7 +287,7 @@ public void loginSuccessCallsOnLoginSuccessCorrectly() { MockRememberMeServices services = new MockRememberMeServices(this.uds); MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); - Authentication auth = new UsernamePasswordAuthenticationToken("joe", "password"); + Authentication auth = UsernamePasswordAuthenticationToken.unauthenticated("joe", "password"); // No parameter set services.loginSuccess(request, response, auth); assertThat(services.loginSuccessCalled).isFalse(); diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java index b79ef32f09a..d6753c60cb4 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -108,7 +108,7 @@ public void loginSuccessCreatesNewTokenAndCookieWithNewSeries() { this.services.setSeriesLength(12); MockHttpServletResponse response = new MockHttpServletResponse(); this.services.loginSuccess(new MockHttpServletRequest(), response, - new UsernamePasswordAuthenticationToken("joe", "password")); + UsernamePasswordAuthenticationToken.unauthenticated("joe", "password")); assertThat(this.repo.getStoredToken().getSeries().length()).isEqualTo(16); assertThat(this.repo.getStoredToken().getTokenValue().length()).isEqualTo(16); String[] cookie = this.services.decodeCookie(response.getCookie("mycookiename").getValue()); diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java index de778a94fd4..44839268bd0 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java @@ -36,10 +36,12 @@ import org.springframework.security.web.authentication.NullRememberMeServices; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.context.SecurityContextRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -152,6 +154,23 @@ public void authenticationSuccessHandlerIsInvokedOnSuccessfulAuthenticationIfSet verifyZeroInteractions(fc); } + @Test + public void securityContextRepositoryInvokedIfSet() throws Exception { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + AuthenticationManager am = mock(AuthenticationManager.class); + given(am.authenticate(this.remembered)).willReturn(this.remembered); + RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter(am, + new MockRememberMeServices(this.remembered)); + filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/target")); + filter.setSecurityContextRepository(securityContextRepository); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain fc = mock(FilterChain.class); + request.setRequestURI("x"); + filter.doFilter(request, response, fc); + verify(securityContextRepository).saveContext(any(), eq(request), eq(response)); + } + private class MockRememberMeServices implements RememberMeServices { private Authentication authToReturn; diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java index bc1aa888c10..09df5e8a535 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java @@ -20,8 +20,6 @@ import javax.servlet.http.Cookie; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +32,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.test.web.CodecTestUtils; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -77,7 +76,7 @@ void udsWillReturnNull() { } private long determineExpiryTimeFromBased64EncodedToken(String validToken) { - String cookieAsPlainText = new String(Base64.decodeBase64(validToken.getBytes())); + String cookieAsPlainText = CodecTestUtils.decodeBase64(validToken); String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":"); if (cookieTokens.length == 3) { try { @@ -93,9 +92,9 @@ private String generateCorrectCookieContentForToken(long expiryTime, String user // format is: // username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + // password + ":" + key) - String signatureValue = DigestUtils.md5Hex(username + ":" + expiryTime + ":" + password + ":" + key); + String signatureValue = CodecTestUtils.md5Hex(username + ":" + expiryTime + ":" + password + ":" + key); String tokenValue = username + ":" + expiryTime + ":" + signatureValue; - return new String(Base64.encodeBase64(tokenValue.getBytes())); + return CodecTestUtils.encodeBase64(tokenValue); } @Test @@ -135,7 +134,7 @@ public void autoLoginReturnsNullForExpiredCookieAndClearsCookie() { @Test public void autoLoginReturnsNullAndClearsCookieIfMissingThreeTokensInCookieValue() { Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, - new String(Base64.encodeBase64("x".getBytes()))); + CodecTestUtils.encodeBase64("x")); MockHttpServletRequest request = new MockHttpServletRequest(); request.setCookies(cookie); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -176,7 +175,7 @@ public void autoLoginClearsCookieIfSignatureBlocksDoesNotMatchExpectedValue() { @Test public void autoLoginClearsCookieIfTokenDoesNotContainANumberInCookieValue() { Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, - new String(Base64.encodeBase64("username:NOT_A_NUMBER:signature".getBytes()))); + CodecTestUtils.encodeBase64("username:NOT_A_NUMBER:signature")); MockHttpServletRequest request = new MockHttpServletRequest(); request.setCookies(cookie); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -276,7 +275,7 @@ public void loginSuccessNormalWithNonUserDetailsBasedPrincipalSetsExpectedCookie assertThat(Long.parseLong(expiryTime) > expectedExpiryTime - 10000).isTrue(); assertThat(cookie).isNotNull(); assertThat(cookie.getMaxAge()).isEqualTo(this.services.getTokenValiditySeconds()); - assertThat(Base64.isArrayByteBase64(cookie.getValue().getBytes())).isTrue(); + assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue(); assertThat(new Date().before(new Date(determineExpiryTimeFromBased64EncodedToken(cookie.getValue())))).isTrue(); } @@ -290,7 +289,7 @@ public void loginSuccessNormalWithUserDetailsBasedPrincipalSetsExpectedCookie() Cookie cookie = response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY); assertThat(cookie).isNotNull(); assertThat(cookie.getMaxAge()).isEqualTo(this.services.getTokenValiditySeconds()); - assertThat(Base64.isArrayByteBase64(cookie.getValue().getBytes())).isTrue(); + assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue(); assertThat(new Date().before(new Date(determineExpiryTimeFromBased64EncodedToken(cookie.getValue())))).isTrue(); } @@ -316,7 +315,7 @@ public void negativeValidityPeriodIsSetOnCookieButExpiryTimeRemainsAtTwoWeeks() assertThat(determineExpiryTimeFromBased64EncodedToken(cookie.getValue()) - System.currentTimeMillis() > AbstractRememberMeServices.TWO_WEEKS_S - 50).isTrue(); assertThat(cookie.getMaxAge()).isEqualTo(-1); - assertThat(Base64.isArrayByteBase64(cookie.getValue().getBytes())).isTrue(); + assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue(); } } diff --git a/web/src/test/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilterTests.java index 1c9f5eaef45..8959f099bac 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilterTests.java @@ -66,7 +66,8 @@ public class SwitchUserFilterTests { @BeforeEach public void authenticateCurrentUser() { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", "hawaii50"); + UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.unauthenticated("dano", + "hawaii50"); SecurityContextHolder.getContext().setAuthentication(auth); } @@ -278,14 +279,14 @@ public void defaultProcessesFilterUrlMatchesUrlWithPathParameter() { @Test public void exitUserJackLordToDanoSucceeds() throws Exception { // original user - UsernamePasswordAuthenticationToken source = new UsernamePasswordAuthenticationToken("dano", "hawaii50", - ROLES_12); + UsernamePasswordAuthenticationToken source = UsernamePasswordAuthenticationToken.authenticated("dano", + "hawaii50", ROLES_12); // set current user (Admin) List adminAuths = new ArrayList<>(); adminAuths.addAll(ROLES_12); adminAuths.add(new SwitchUserGrantedAuthority("PREVIOUS_ADMINISTRATOR", source)); - UsernamePasswordAuthenticationToken admin = new UsernamePasswordAuthenticationToken("jacklord", "hawaii50", - adminAuths); + UsernamePasswordAuthenticationToken admin = UsernamePasswordAuthenticationToken.authenticated("jacklord", + "hawaii50", adminAuths); SecurityContextHolder.getContext().setAuthentication(admin); MockHttpServletRequest request = createMockSwitchRequest(); request.setRequestURI("/logout/impersonate"); @@ -343,7 +344,8 @@ public void redirectToTargetUrlIsCorrect() throws Exception { @Test public void redirectOmitsContextPathIfUseRelativeContextSet() throws Exception { // set current user - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", "hawaii50"); + UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.unauthenticated("dano", + "hawaii50"); SecurityContextHolder.getContext().setAuthentication(auth); MockHttpServletRequest request = createMockSwitchRequest(); request.setContextPath("/webapp"); @@ -368,7 +370,8 @@ public void redirectOmitsContextPathIfUseRelativeContextSet() throws Exception { @Test public void testSwitchRequestFromDanoToJackLord() throws Exception { // set current user - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", "hawaii50"); + UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.unauthenticated("dano", + "hawaii50"); SecurityContextHolder.getContext().setAuthentication(auth); // http request MockHttpServletRequest request = new MockHttpServletRequest(); @@ -395,7 +398,8 @@ public void testSwitchRequestFromDanoToJackLord() throws Exception { @Test public void modificationOfAuthoritiesWorks() { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", "hawaii50"); + UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.unauthenticated("dano", + "hawaii50"); SecurityContextHolder.getContext().setAuthentication(auth); MockHttpServletRequest request = new MockHttpServletRequest(); request.addParameter(SwitchUserFilter.SPRING_SECURITY_SWITCH_USERNAME_KEY, "jacklord"); @@ -416,8 +420,8 @@ public void modificationOfAuthoritiesWorks() { @Test public void nestedSwitchesAreNotAllowed() { // original user - UsernamePasswordAuthenticationToken source = new UsernamePasswordAuthenticationToken("orig", "hawaii50", - ROLES_12); + UsernamePasswordAuthenticationToken source = UsernamePasswordAuthenticationToken.authenticated("orig", + "hawaii50", ROLES_12); SecurityContextHolder.getContext().setAuthentication(source); SecurityContextHolder.getContext().setAuthentication(switchToUser("jacklord")); Authentication switched = switchToUser("dano"); @@ -444,8 +448,8 @@ public void switchAuthorityRoleCannotBeNull() { public void switchAuthorityRoleCanBeChanged() { String switchAuthorityRole = "PREVIOUS_ADMINISTRATOR"; // original user - UsernamePasswordAuthenticationToken source = new UsernamePasswordAuthenticationToken("orig", "hawaii50", - ROLES_12); + UsernamePasswordAuthenticationToken source = UsernamePasswordAuthenticationToken.authenticated("orig", + "hawaii50", ROLES_12); SecurityContextHolder.getContext().setAuthentication(source); SecurityContextHolder.getContext().setAuthentication(switchToUser("jacklord")); Authentication switched = switchToUserWithAuthorityRole("dano", switchAuthorityRole); diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverterTests.java index 744f32993a2..3561b3e9ad7 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationConverterTests.java @@ -18,7 +18,6 @@ import javax.servlet.http.HttpServletRequest; -import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.test.web.CodecTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -57,7 +57,7 @@ public void setup() { public void testNormalOperation() { String token = "rod:koala"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); UsernamePasswordAuthenticationToken authentication = this.converter.convert(request); verify(this.authenticationDetailsSource).buildDetails(any()); assertThat(authentication).isNotNull(); @@ -68,7 +68,7 @@ public void testNormalOperation() { public void requestWhenAuthorizationSchemeInMixedCaseThenAuthenticates() { String token = "rod:koala"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "BaSiC " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "BaSiC " + CodecTestUtils.encodeBase64(token)); UsernamePasswordAuthenticationToken authentication = this.converter.convert(request); verify(this.authenticationDetailsSource).buildDetails(any()); assertThat(authentication).isNotNull(); @@ -88,7 +88,7 @@ public void testWhenUnsupportedAuthorizationHeaderThenIgnored() { public void testWhenInvalidBasicAuthorizationTokenThenError() { String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.converter.convert(request)); } @@ -103,7 +103,7 @@ public void testWhenInvalidBase64ThenError() { public void convertWhenEmptyPassword() { String token = "rod:"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); UsernamePasswordAuthenticationToken authentication = this.converter.convert(request); verify(this.authenticationDetailsSource).buildDetails(any()); assertThat(authentication).isNotNull(); diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java index 13b265b8e00..e8b6ed76c96 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java @@ -23,10 +23,10 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -36,8 +36,11 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.web.CodecTestUtils; import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -64,9 +67,10 @@ public class BasicAuthenticationFilterTests { @BeforeEach public void setUp() { SecurityContextHolder.clearContext(); - UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "koala"); + UsernamePasswordAuthenticationToken rodRequest = UsernamePasswordAuthenticationToken.unauthenticated("rod", + "koala"); rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest())); - Authentication rod = new UsernamePasswordAuthenticationToken("rod", "koala", + Authentication rod = UsernamePasswordAuthenticationToken.authenticated("rod", "koala", AuthorityUtils.createAuthorityList("ROLE_1")); this.manager = mock(AuthenticationManager.class); given(this.manager.authenticate(rodRequest)).willReturn(rod); @@ -101,7 +105,7 @@ public void testGettersSetters() { public void testInvalidBasicAuthorizationTokenIsIgnored() throws Exception { String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); request.setSession(new MockHttpSession()); final MockHttpServletResponse response = new MockHttpServletResponse(); @@ -131,7 +135,7 @@ public void invalidBase64IsIgnored() throws Exception { public void testNormalOperation() throws Exception { String token = "rod:koala"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); // Test assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); @@ -147,7 +151,7 @@ public void testNormalOperation() throws Exception { public void doFilterWhenSchemeLowercaseThenCaseInsensitveMatchWorks() throws Exception { String token = "rod:koala"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); // Test assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); @@ -162,7 +166,7 @@ public void doFilterWhenSchemeLowercaseThenCaseInsensitveMatchWorks() throws Exc public void doFilterWhenSchemeMixedCaseThenCaseInsensitiveMatchWorks() throws Exception { String token = "rod:koala"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "BaSiC " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "BaSiC " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); FilterChain chain = mock(FilterChain.class); @@ -197,7 +201,7 @@ public void testStartupDetectsMissingAuthenticationManager() { public void testSuccessLoginThenFailureLoginResultsInSessionLosingToken() throws Exception { String token = "rod:koala"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); final MockHttpServletResponse response1 = new MockHttpServletResponse(); FilterChain chain = mock(FilterChain.class); @@ -209,7 +213,7 @@ public void testSuccessLoginThenFailureLoginResultsInSessionLosingToken() throws // NOW PERFORM FAILED AUTHENTICATION token = "otherUser:WRONG_PASSWORD"; request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); final MockHttpServletResponse response2 = new MockHttpServletResponse(); chain = mock(FilterChain.class); this.filter.doFilter(request, response2, chain); @@ -225,7 +229,7 @@ public void testSuccessLoginThenFailureLoginResultsInSessionLosingToken() throws public void testWrongPasswordContinuesFilterChainIfIgnoreFailureIsTrue() throws Exception { String token = "rod:WRONG_PASSWORD"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); request.setSession(new MockHttpSession()); this.filter = new BasicAuthenticationFilter(this.manager); @@ -241,7 +245,7 @@ public void testWrongPasswordContinuesFilterChainIfIgnoreFailureIsTrue() throws public void testWrongPasswordReturnsForbiddenIfIgnoreFailureIsFalse() throws Exception { String token = "rod:WRONG_PASSWORD"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); request.setSession(new MockHttpSession()); assertThat(this.filter.isIgnoreFailure()).isFalse(); @@ -259,7 +263,7 @@ public void testWrongPasswordReturnsForbiddenIfIgnoreFailureIsFalse() throws Exc public void skippedOnErrorDispatch() throws Exception { String token = "bad:credentials"; MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); request.setServletPath("/some_file.html"); request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -271,9 +275,10 @@ public void skippedOnErrorDispatch() throws Exception { @Test public void doFilterWhenTokenAndFilterCharsetMatchDefaultThenAuthenticated() throws Exception { SecurityContextHolder.clearContext(); - UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü"); + UsernamePasswordAuthenticationToken rodRequest = UsernamePasswordAuthenticationToken.unauthenticated("rod", + "äöü"); rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest())); - Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", + Authentication rod = UsernamePasswordAuthenticationToken.authenticated("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1")); this.manager = mock(AuthenticationManager.class); given(this.manager.authenticate(rodRequest)).willReturn(rod); @@ -282,7 +287,7 @@ public void doFilterWhenTokenAndFilterCharsetMatchDefaultThenAuthenticated() thr String token = "rod:äöü"; MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", - "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.UTF_8)))); + "Basic " + CodecTestUtils.encodeBase64(token.getBytes(StandardCharsets.UTF_8))); request.setServletPath("/some_file.html"); MockHttpServletResponse response = new MockHttpServletResponse(); // Test @@ -298,9 +303,10 @@ public void doFilterWhenTokenAndFilterCharsetMatchDefaultThenAuthenticated() thr @Test public void doFilterWhenTokenAndFilterCharsetMatchNonDefaultThenAuthenticated() throws Exception { SecurityContextHolder.clearContext(); - UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü"); + UsernamePasswordAuthenticationToken rodRequest = UsernamePasswordAuthenticationToken.unauthenticated("rod", + "äöü"); rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest())); - Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", + Authentication rod = UsernamePasswordAuthenticationToken.authenticated("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1")); this.manager = mock(AuthenticationManager.class); given(this.manager.authenticate(rodRequest)).willReturn(rod); @@ -310,7 +316,7 @@ public void doFilterWhenTokenAndFilterCharsetMatchNonDefaultThenAuthenticated() String token = "rod:äöü"; MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", - "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.ISO_8859_1)))); + "Basic " + CodecTestUtils.encodeBase64(token.getBytes(StandardCharsets.ISO_8859_1))); request.setServletPath("/some_file.html"); MockHttpServletResponse response = new MockHttpServletResponse(); // Test @@ -326,9 +332,10 @@ public void doFilterWhenTokenAndFilterCharsetMatchNonDefaultThenAuthenticated() @Test public void doFilterWhenTokenAndFilterCharsetDoNotMatchThenUnauthorized() throws Exception { SecurityContextHolder.clearContext(); - UsernamePasswordAuthenticationToken rodRequest = new UsernamePasswordAuthenticationToken("rod", "äöü"); + UsernamePasswordAuthenticationToken rodRequest = UsernamePasswordAuthenticationToken.unauthenticated("rod", + "äöü"); rodRequest.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest())); - Authentication rod = new UsernamePasswordAuthenticationToken("rod", "äöü", + Authentication rod = UsernamePasswordAuthenticationToken.authenticated("rod", "äöü", AuthorityUtils.createAuthorityList("ROLE_1")); this.manager = mock(AuthenticationManager.class); given(this.manager.authenticate(rodRequest)).willReturn(rod); @@ -338,7 +345,7 @@ public void doFilterWhenTokenAndFilterCharsetDoNotMatchThenUnauthorized() throws String token = "rod:äöü"; MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", - "Basic " + new String(Base64.encodeBase64(token.getBytes(StandardCharsets.UTF_8)))); + "Basic " + CodecTestUtils.encodeBase64(token.getBytes(StandardCharsets.UTF_8))); request.setServletPath("/some_file.html"); MockHttpServletResponse response = new MockHttpServletResponse(); // Test @@ -364,4 +371,25 @@ public void requestWhenEmptyBasicAuthorizationHeaderTokenThenUnauthorized() thro assertThat(response.getStatus()).isEqualTo(401); } + @Test + public void requestWhenSecurityContextRepository() throws Exception { + ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + this.filter.setSecurityContextRepository(securityContextRepository); + String token = "rod:koala"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); + request.setServletPath("/some_file.html"); + MockHttpServletResponse response = new MockHttpServletResponse(); + // Test + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + FilterChain chain = mock(FilterChain.class); + this.filter.doFilter(request, response, chain); + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod"); + verify(securityContextRepository).saveContext(contextArg.capture(), eq(request), eq(response)); + assertThat(contextArg.getValue().getAuthentication().getName()).isEqualTo("rod"); + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java index 94cd7b8887d..1e9f557c0d7 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java @@ -18,13 +18,12 @@ import java.util.Map; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.DisabledException; +import org.springframework.security.test.web.CodecTestUtils; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -41,11 +40,11 @@ private void checkNonceValid(String nonce) { // Check the nonce seems to be generated correctly // format of nonce is: // base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) - assertThat(Base64.isArrayByteBase64(nonce.getBytes())).isTrue(); - String decodedNonce = new String(Base64.decodeBase64(nonce.getBytes())); + assertThat(CodecTestUtils.isBase64(nonce.getBytes())).isTrue(); + String decodedNonce = CodecTestUtils.decodeBase64(nonce); String[] nonceTokens = StringUtils.delimitedListToStringArray(decodedNonce, ":"); assertThat(nonceTokens).hasSize(2); - String expectedNonceSignature = DigestUtils.md5Hex(nonceTokens[0] + ":" + "key"); + String expectedNonceSignature = CodecTestUtils.md5Hex(nonceTokens[0] + ":" + "key"); assertThat(nonceTokens[1]).isEqualTo(expectedNonceSignature); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java index 6168db71433..044ab64a542 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java @@ -24,11 +24,10 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -40,10 +39,13 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.cache.NullUserCache; +import org.springframework.security.test.web.CodecTestUtils; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -103,9 +105,9 @@ private static String generateNonce(int validitySeconds) { private static String generateNonce(int validitySeconds, String key) { long expiryTime = System.currentTimeMillis() + (validitySeconds * 1000); - String signatureValue = DigestUtils.md5Hex(expiryTime + ":" + key); + String signatureValue = CodecTestUtils.md5Hex(expiryTime + ":" + key); String nonceValue = expiryTime + ":" + signatureValue; - return new String(Base64.encodeBase64(nonceValue.getBytes())); + return CodecTestUtils.encodeBase64(nonceValue); } @AfterEach @@ -180,7 +182,7 @@ public void testGettersSetters() { @Test public void testInvalidDigestAuthorizationTokenGeneratesError() throws Exception { String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON"; - this.request.addHeader("Authorization", "Digest " + new String(Base64.encodeBase64(token.getBytes()))); + this.request.addHeader("Authorization", "Digest " + CodecTestUtils.encodeBase64(token)); MockHttpServletResponse response = executeFilterInContainerSimulator(this.filter, this.request, false); assertThat(response.getStatus()).isEqualTo(401); assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); @@ -208,7 +210,7 @@ public void testNonBase64EncodedNonceReturnsForbidden() throws Exception { @Test public void testNonceWithIncorrectSignatureForNumericFieldReturnsForbidden() throws Exception { - String nonce = new String(Base64.encodeBase64("123456:incorrectStringPassword".getBytes())); + String nonce = CodecTestUtils.encodeBase64("123456:incorrectStringPassword"); String responseDigest = DigestAuthUtils.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", REQUEST_URI, QOP, nonce, NC, CNONCE); this.request.addHeader("Authorization", @@ -220,7 +222,7 @@ public void testNonceWithIncorrectSignatureForNumericFieldReturnsForbidden() thr @Test public void testNonceWithNonNumericFirstElementReturnsForbidden() throws Exception { - String nonce = new String(Base64.encodeBase64("hello:ignoredSecondElement".getBytes())); + String nonce = CodecTestUtils.encodeBase64("hello:ignoredSecondElement"); String responseDigest = DigestAuthUtils.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", REQUEST_URI, QOP, nonce, NC, CNONCE); this.request.addHeader("Authorization", @@ -232,7 +234,7 @@ public void testNonceWithNonNumericFirstElementReturnsForbidden() throws Excepti @Test public void testNonceWithoutTwoColonSeparatedElementsReturnsForbidden() throws Exception { - String nonce = new String(Base64.encodeBase64("a base 64 string without a colon".getBytes())); + String nonce = CodecTestUtils.encodeBase64("a base 64 string without a colon"); String responseDigest = DigestAuthUtils.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", REQUEST_URI, QOP, nonce, NC, CNONCE); this.request.addHeader("Authorization", @@ -389,4 +391,25 @@ public void authenticationCreatesEmptyContext() throws Exception { assertThat(existingAuthentication).isSameAs(existingContext.getAuthentication()); } + @Test + public void testSecurityContextRepository() throws Exception { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); + String responseDigest = DigestAuthUtils.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", REQUEST_URI, + QOP, NONCE, NC, CNONCE); + this.request.addHeader("Authorization", + createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); + this.filter.setSecurityContextRepository(securityContextRepository); + this.filter.setCreateAuthenticatedToken(true); + MockHttpServletResponse response = executeFilterInContainerSimulator(this.filter, this.request, true); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()) + .isEqualTo(USERNAME); + assertThat(SecurityContextHolder.getContext().getAuthentication().isAuthenticated()).isTrue(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getAuthorities()) + .isEqualTo(AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + verify(securityContextRepository).saveContext(contextArg.capture(), eq(this.request), eq(response)); + assertThat(contextArg.getValue().getAuthentication().getName()).isEqualTo(USERNAME); + } + } diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index cfbe0f332f4..26cf33f13e4 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Collections; import javax.servlet.Filter; import javax.servlet.ServletException; @@ -42,25 +43,28 @@ import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.Transient; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.context.TransientSecurityContext; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Luke Taylor @@ -131,6 +135,41 @@ public void sessionIsntCreatedIfAllowSessionCreationIsFalse() { assertThat(request.getSession(false)).isNull(); } + @Test + public void loadContextWhenNullResponse() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, null); + assertThat(repo.loadContext(holder)).isEqualTo(SecurityContextHolder.createEmptyContext()); + } + + @Test + public void loadContextHttpServletRequestWhenNotSavedThenEmptyContextReturned() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + assertThat(repo.loadContext(request).get()).isEqualTo(SecurityContextHolder.createEmptyContext()); + } + + @Test + public void loadContextHttpServletRequestWhenSavedThenSavedContextReturned() { + SecurityContextImpl expectedContext = new SecurityContextImpl(this.testToken); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + repo.saveContext(expectedContext, request, response); + assertThat(repo.loadContext(request).get()).isEqualTo(expectedContext); + } + + @Test + public void loadContextHttpServletRequestWhenNotAccessedThenHttpSessionNotAccessed() { + HttpSession session = mock(HttpSession.class); + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setSession(session); + repo.loadContext(request); + verifyNoInteractions(session); + } + @Test public void existingContextIsSuccessFullyLoadedFromSessionAndSavedBack() { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); @@ -577,13 +616,78 @@ public void traverseWrappedRequests() { } @Test - public void failsWithStandardResponse() { + public void standardResponseWorks() { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(this.testToken); - assertThatIllegalStateException().isThrownBy(() -> repo.saveContext(context, request, response)); + repo.saveContext(context, request, response); + assertThat(request.getSession(false)).isNotNull(); + assertThat(request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)) + .isEqualTo(context); + } + + @Test + public void saveContextWhenTransientSecurityContextThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContext context = repo.loadContext(holder); + SecurityContext transientSecurityContext = new TransientSecurityContext(); + Authentication authentication = TestAuthentication.authenticatedUser(); + transientSecurityContext.setAuthentication(authentication); + repo.saveContext(transientSecurityContext, holder.getRequest(), holder.getResponse()); + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientSecurityContextSubclassThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContext context = repo.loadContext(holder); + SecurityContext transientSecurityContext = new TransientSecurityContext() { + }; + Authentication authentication = TestAuthentication.authenticatedUser(); + transientSecurityContext.setAuthentication(authentication); + repo.saveContext(transientSecurityContext, holder.getRequest(), holder.getResponse()); + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientSecurityContextAndSessionExistsThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession(); // ensure the session exists + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContext context = repo.loadContext(holder); + SecurityContext transientSecurityContext = new TransientSecurityContext(); + Authentication authentication = TestAuthentication.authenticatedUser(); + transientSecurityContext.setAuthentication(authentication); + repo.saveContext(transientSecurityContext, holder.getRequest(), holder.getResponse()); + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(Collections.list(session.getAttributeNames())).isEmpty(); + } + + @Test + public void saveContextWhenTransientSecurityContextWithCustomAnnotationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContext context = repo.loadContext(holder); + SecurityContext transientSecurityContext = new TransientSecurityContext(); + Authentication authentication = TestAuthentication.authenticatedUser(); + transientSecurityContext.setAuthentication(authentication); + repo.saveContext(transientSecurityContext, holder.getRequest(), holder.getResponse()); + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); } @Test @@ -614,6 +718,21 @@ public void saveContextWhenTransientAuthenticationSubclassThenSkipped() { assertThat(session).isNull(); } + @Test + public void saveContextWhenTransientAuthenticationAndSessionExistsThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession(); // ensure the session exists + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContext context = repo.loadContext(holder); + SomeTransientAuthentication authentication = new SomeTransientAuthentication(); + context.setAuthentication(authentication); + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(Collections.list(session.getAttributeNames())).isEmpty(); + } + @Test public void saveContextWhenTransientAuthenticationWithCustomAnnotationThenSkipped() { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); @@ -645,7 +764,7 @@ public void saveContextWhenSecurityContextAuthenticationUpdatedToNullThenSkipped } private SecurityContext createSecurityContext(UserDetails userDetails) { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); SecurityContext securityContext = new SecurityContextImpl(token); return securityContext; diff --git a/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java new file mode 100644 index 00000000000..5fc4d4afb7d --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.context; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Winch + */ +class RequestAttributeSecurityContextRepositoryTests { + + private MockHttpServletRequest request = new MockHttpServletRequest(); + + private MockHttpServletResponse response = new MockHttpServletResponse(); + + private RequestAttributeSecurityContextRepository repository = new RequestAttributeSecurityContextRepository(); + + private SecurityContext expectedSecurityContext = new SecurityContextImpl(TestAuthentication.authenticatedUser()); + + @Test + void saveContextAndLoadContextThenFound() { + this.repository.saveContext(this.expectedSecurityContext, this.request, this.response); + SecurityContext securityContext = this.repository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)); + assertThat(securityContext).isEqualTo(this.expectedSecurityContext); + } + + @Test + void saveContextWhenLoadContextAndNewRequestThenNotFound() { + this.repository.saveContext(this.expectedSecurityContext, this.request, this.response); + SecurityContext securityContext = this.repository.loadContext( + new HttpRequestResponseHolder(new MockHttpServletRequest(), new MockHttpServletResponse())); + assertThat(securityContext).isEqualTo(SecurityContextHolder.createEmptyContext()); + } + + @Test + void containsContextWhenNotSavedThenFalse() { + assertThat(this.repository.containsContext(this.request)).isFalse(); + } + + @Test + void containsContextWhenSavedThenTrue() { + this.repository.saveContext(this.expectedSecurityContext, this.request, this.response); + assertThat(this.repository.containsContext(this.request)).isTrue(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java b/web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java new file mode 100644 index 00000000000..0ed4e1d9ec5 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.context; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class SecurityContextHolderFilterTests { + + @Mock + private SecurityContextRepository repository; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Captor + private ArgumentCaptor requestArg; + + private SecurityContextHolderFilter filter; + + @BeforeEach + void setup() { + this.filter = new SecurityContextHolderFilter(this.repository); + } + + @AfterEach + void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + void doFilterThenSetsAndClearsSecurityContextHolder() throws Exception { + Authentication authentication = TestAuthentication.authenticatedUser(); + SecurityContext expectedContext = new SecurityContextImpl(authentication); + given(this.repository.loadContext(this.requestArg.capture())).willReturn(() -> expectedContext); + FilterChain filterChain = (request, response) -> assertThat(SecurityContextHolder.getContext()) + .isEqualTo(expectedContext); + + this.filter.doFilter(this.request, this.response, filterChain); + + assertThat(SecurityContextHolder.getContext()).isEqualTo(SecurityContextHolder.createEmptyContext()); + } + + @Test + void shouldNotFilterErrorDispatchWhenDefault() { + assertThat(this.filter.shouldNotFilterErrorDispatch()).isFalse(); + } + + @Test + void shouldNotFilterErrorDispatchWhenOverridden() { + this.filter.setShouldNotFilterErrorDispatch(true); + assertThat(this.filter.shouldNotFilterErrorDispatch()).isTrue(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java b/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java new file mode 100644 index 00000000000..ba98649c431 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +public class CompositeRequestRejectedHandlerTests { + + @Test + void compositeRequestRejectedHandlerRethrowsTheException() { + RequestRejectedException requestRejectedException = new RequestRejectedException("rejected"); + CompositeRequestRejectedHandler handler = new CompositeRequestRejectedHandler( + new DefaultRequestRejectedHandler()); + assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> handler + .handle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), requestRejectedException)) + .withMessage("rejected"); + } + + @Test + void compositeRequestRejectedHandlerForbidsEmptyHandlers() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(CompositeRequestRejectedHandler::new); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java index ce461e34015..1115a3bcd70 100644 --- a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java +++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java @@ -343,6 +343,12 @@ public void getFirewalledRequestWhenContainsUpperboundAsciiThenNoException() { this.firewall.getFirewalledRequest(this.request); } + @Test + public void getFirewalledRequestWhenJapaneseCharacterThenNoException() { + this.request.setServletPath("/\u3042"); + this.firewall.getFirewalledRequest(this.request); + } + @Test public void getFirewalledRequestWhenExceedsUpperboundAsciiThenException() { this.request.setRequestURI("/\u007f"); @@ -364,6 +370,152 @@ public void getFirewalledRequestWhenContainsEncodedNullThenException() { .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); } + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedThenException() { + this.request.setRequestURI("/something%0a/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedThenException() { + this.request.setRequestURI("/something%0A/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsLineFeedThenException() { + this.request.setRequestURI("/something\n/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineFeedThenException() { + this.request.setServletPath("/something\n/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnThenException() { + this.request.setRequestURI("/something%0d/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnThenException() { + this.request.setRequestURI("/something%0D/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsCarriageReturnThenException() { + this.request.setRequestURI("/something\r/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsCarriageReturnThenException() { + this.request.setServletPath("/something\r/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineSeparatorThenException() { + this.request.setServletPath("/something\u2028/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorThenException() { + this.request.setServletPath("/something\u2029/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setRequestURI("/something%0a/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setRequestURI("/something%0A/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsLineFeedAndAllowedThenException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setRequestURI("/something\n/"); + // Expected an error because the line feed is decoded in an encoded part of the + // URL + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineFeedAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setServletPath("/something\n/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setRequestURI("/something%0d/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setRequestURI("/something%0D/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setRequestURI("/something\r/"); + // Expected an error because the carriage return is decoded in an encoded part of + // the URL + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setServletPath("/something\r/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineSeparatorAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineSeparator(true); + this.request.setServletPath("/something\u2028/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedParagraphSeparator(true); + this.request.setServletPath("/something\u2029/"); + this.firewall.getFirewalledRequest(this.request); + } + /** * On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on /a/b/c * because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c while Spring MVC diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java new file mode 100644 index 00000000000..0b90c57dea3 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginEmbedderPolicyHeaderWriterTests { + + private static final String EMBEDDER_HEADER_NAME = "Cross-Origin-Embedder-Policy"; + + private CrossOriginEmbedderPolicyHeaderWriter writer; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + void setup() { + this.writer = new CrossOriginEmbedderPolicyHeaderWriter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + void setEmbedderPolicyWhenNullEmbedderPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("embedderPolicy cannot be null"); + } + + @Test + void writeHeadersWhenDefaultValuesThenDontWriteHeaders() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(0); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.response.addHeader(EMBEDDER_HEADER_NAME, "require-corp"); + this.writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.UNSAFE_NONE); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(EMBEDDER_HEADER_NAME)).isEqualTo("require-corp"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(EMBEDDER_HEADER_NAME)).isEqualTo("require-corp"); + } + + @Test + void writeHeadersWhenSetEmbedderPolicyThenWritesEmbedderPolicy() { + this.writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.UNSAFE_NONE); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader(EMBEDDER_HEADER_NAME)).isEqualTo("unsafe-none"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java new file mode 100644 index 00000000000..863351bb8b1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginOpenerPolicyHeaderWriterTests { + + private static final String OPENER_HEADER_NAME = "Cross-Origin-Opener-Policy"; + + private CrossOriginOpenerPolicyHeaderWriter writer; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + void setup() { + this.writer = new CrossOriginOpenerPolicyHeaderWriter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + void setOpenerPolicyWhenNullOpenerPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("openerPolicy cannot be null"); + } + + @Test + void writeHeadersWhenDefaultValuesThenDontWriteHeaders() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(0); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.response.addHeader(OPENER_HEADER_NAME, "same-origin"); + this.writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(OPENER_HEADER_NAME)).isEqualTo("same-origin"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(OPENER_HEADER_NAME)).isEqualTo("same-origin-allow-popups"); + } + + @Test + void writeHeadersWhenSetOpenerPolicyThenWritesOpenerPolicy() { + this.writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader(OPENER_HEADER_NAME)).isEqualTo("same-origin-allow-popups"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java new file mode 100644 index 00000000000..14b8f04a031 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginResourcePolicyHeaderWriterTests { + + private static final String RESOURCE_HEADER_NAME = "Cross-Origin-Resource-Policy"; + + private CrossOriginResourcePolicyHeaderWriter writer; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + void setup() { + this.writer = new CrossOriginResourcePolicyHeaderWriter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + void setResourcePolicyWhenNullThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("resourcePolicy cannot be null"); + } + + @Test + void writeHeadersWhenDefaultValuesThenDontWriteHeaders() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(0); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.response.addHeader(RESOURCE_HEADER_NAME, "same-site"); + this.writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.CROSS_ORIGIN); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(RESOURCE_HEADER_NAME)).isEqualTo("same-site"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(RESOURCE_HEADER_NAME)).isEqualTo("same-origin"); + } + + @Test + void writeHeadersWhenSetResourcePolicyThenWritesResourcePolicy() { + this.writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_SITE); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader(RESOURCE_HEADER_NAME)).isEqualTo("same-site"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java index 8a3d2f7c437..6b43155abe5 100644 --- a/web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java +++ b/web/src/test/java/org/springframework/security/web/jackson2/WebAuthenticationDetailsMixinTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-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. @@ -64,6 +64,13 @@ public void webAuthenticationDetailsSerializeTest() throws JsonProcessingExcepti JSONAssert.assertEquals(AUTHENTICATION_DETAILS_JSON, actualJson, true); } + @Test + public void webAuthenticationDetailsJackson2SerializeTest() throws JsonProcessingException, JSONException { + WebAuthenticationDetails details = new WebAuthenticationDetails("/localhost", "1"); + String actualJson = this.mapper.writeValueAsString(details); + JSONAssert.assertEquals(AUTHENTICATION_DETAILS_JSON, actualJson, true); + } + @Test public void webAuthenticationDetailsDeserializeTest() throws IOException { WebAuthenticationDetails details = this.mapper.readValue(AUTHENTICATION_DETAILS_JSON, diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/ServerHttpBasicAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/ServerHttpBasicAuthenticationConverterTests.java index 1f4a0d2a3f5..d735311a7f5 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/ServerHttpBasicAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/ServerHttpBasicAuthenticationConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.server.authentication; +import java.nio.charset.StandardCharsets; + import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -26,6 +28,7 @@ import org.springframework.security.core.Authentication; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * @author Rob Winch @@ -37,6 +40,12 @@ public class ServerHttpBasicAuthenticationConverterTests { MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest.get("/"); + @Test + public void setCredentialsCharsetWhenNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setCredentialsCharset(null)) + .withMessage("credentialsCharset cannot be null"); + } + @Test public void applyWhenNoAuthorizationHeaderThenEmpty() { Mono result = apply(this.request); @@ -62,7 +71,7 @@ public void applyWhenNotBase64ThenEmpty() { } @Test - public void applyWhenNoSemicolonThenEmpty() { + public void applyWhenNoColonThenEmpty() { Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcg==")); assertThat(result.block()).isNull(); } @@ -104,6 +113,38 @@ public void applyWhenWrongSchemeThenEmpty() { assertThat(result.block()).isNull(); } + @Test + public void applyWhenNonAsciiThenAuthentication() { + Mono result = apply( + this.request.header(HttpHeaders.AUTHORIZATION, "Basic w7xzZXI6cGFzc3fDtnJk")); + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class) + .block(); + assertThat(authentication.getPrincipal()).isEqualTo("üser"); + assertThat(authentication.getCredentials()).isEqualTo("passwörd"); + } + + @Test + public void applyWhenIsoOnlyAsciiThenAuthentication() { + this.converter.setCredentialsCharset(StandardCharsets.ISO_8859_1); + Mono result = apply( + this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")); + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class) + .block(); + assertThat(authentication.getPrincipal()).isEqualTo("user"); + assertThat(authentication.getCredentials()).isEqualTo("password"); + } + + @Test + public void applyWhenIsoNonAsciiThenAuthentication() { + this.converter.setCredentialsCharset(StandardCharsets.ISO_8859_1); + Mono result = apply( + this.request.header(HttpHeaders.AUTHORIZATION, "Basic /HNlcjpwYXNzd/ZyZA==")); + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class) + .block(); + assertThat(authentication.getPrincipal()).isEqualTo("üser"); + assertThat(authentication.getCredentials()).isEqualTo("passwörd"); + } + private Mono apply(MockServerHttpRequest.BaseBuilder request) { return this.converter.convert(MockServerWebExchange.from(this.request.build())); } diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java new file mode 100644 index 00000000000..f40b794ee24 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} + * + * @author Josh Cummings + */ +public class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests { + + private ReactiveAuthenticationManager one = mock(ReactiveAuthenticationManager.class); + + private ReactiveAuthenticationManager two = mock(ReactiveAuthenticationManager.class); + + @Test + public void resolveWhenMatchesThenReturnsReactiveAuthenticationManager() { + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver resolver = ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver + .builder().add(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one) + .add(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two).build(); + + MockServerHttpRequest request = MockServerHttpRequest.get("/one/location").build(); + assertThat(resolver.resolve(MockServerWebExchange.from(request)).block()).isEqualTo(this.one); + } + + @Test + public void resolveWhenDoesNotMatchThenReturnsDefaultReactiveAuthenticationManager() { + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver resolver = ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver + .builder().add(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one) + .add(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two).build(); + + MockServerHttpRequest request = MockServerHttpRequest.get("/wrong/location").build(); + ReactiveAuthenticationManager authenticationManager = resolver.resolve(MockServerWebExchange.from(request)) + .block(); + + Authentication authentication = new TestingAuthenticationToken("principal", "creds"); + assertThatExceptionOfType(AuthenticationServiceException.class) + .isThrownBy(() -> authenticationManager.authenticate(authentication).block()); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/SwitchUserWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/SwitchUserWebFilterTests.java index 527d90345d0..4e7609f3031 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/SwitchUserWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/SwitchUserWebFilterTests.java @@ -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. @@ -110,7 +110,7 @@ public void switchUser() { final MockServerWebExchange exchange = MockServerWebExchange .from(MockServerHttpRequest.post("/login/impersonate?username={targetUser}", targetUsername)); final WebFilterChain chain = mock(WebFilterChain.class); - final Authentication originalAuthentication = new UsernamePasswordAuthenticationToken("principal", + final Authentication originalAuthentication = UsernamePasswordAuthenticationToken.unauthenticated("principal", "credentials"); final SecurityContextImpl securityContext = new SecurityContextImpl(originalAuthentication); given(this.userDetailsService.findByUsername(targetUsername)).willReturn(Mono.just(switchUserDetails)); @@ -143,12 +143,12 @@ public void switchUser() { @Test public void switchUserWhenUserAlreadySwitchedThenExitSwitchAndSwitchAgain() { - final Authentication originalAuthentication = new UsernamePasswordAuthenticationToken("origPrincipal", - "origCredentials"); + final Authentication originalAuthentication = UsernamePasswordAuthenticationToken + .unauthenticated("origPrincipal", "origCredentials"); final GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority( SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR, originalAuthentication); - final Authentication switchUserAuthentication = new UsernamePasswordAuthenticationToken("switchPrincipal", - "switchCredentials", Collections.singleton(switchAuthority)); + final Authentication switchUserAuthentication = UsernamePasswordAuthenticationToken + .authenticated("switchPrincipal", "switchCredentials", Collections.singleton(switchAuthority)); final SecurityContextImpl securityContext = new SecurityContextImpl(switchUserAuthentication); final String targetUsername = "newSwitchPrincipal"; final MockServerWebExchange exchange = MockServerWebExchange @@ -228,12 +228,12 @@ public void switchUserWhenFailureHandlerNotDefinedThenReturnError() { public void exitSwitchThenReturnToOriginalAuthentication() { final MockServerWebExchange exchange = MockServerWebExchange .from(MockServerHttpRequest.post("/logout/impersonate")); - final Authentication originalAuthentication = new UsernamePasswordAuthenticationToken("origPrincipal", - "origCredentials"); + final Authentication originalAuthentication = UsernamePasswordAuthenticationToken + .unauthenticated("origPrincipal", "origCredentials"); final GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority( SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR, originalAuthentication); - final Authentication switchUserAuthentication = new UsernamePasswordAuthenticationToken("switchPrincipal", - "switchCredentials", Collections.singleton(switchAuthority)); + final Authentication switchUserAuthentication = UsernamePasswordAuthenticationToken + .authenticated("switchPrincipal", "switchCredentials", Collections.singleton(switchAuthority)); final WebFilterChain chain = mock(WebFilterChain.class); final SecurityContextImpl securityContext = new SecurityContextImpl(switchUserAuthentication); given(this.serverSecurityContextRepository.save(eq(exchange), any(SecurityContext.class))) @@ -259,8 +259,8 @@ public void exitSwitchThenReturnToOriginalAuthentication() { public void exitSwitchWhenUserNotSwitchedThenThrowError() { final MockServerWebExchange exchange = MockServerWebExchange .from(MockServerHttpRequest.post("/logout/impersonate")); - final Authentication originalAuthentication = new UsernamePasswordAuthenticationToken("origPrincipal", - "origCredentials"); + final Authentication originalAuthentication = UsernamePasswordAuthenticationToken + .unauthenticated("origPrincipal", "origCredentials"); final WebFilterChain chain = mock(WebFilterChain.class); final SecurityContextImpl securityContext = new SecurityContextImpl(originalAuthentication); assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class).isThrownBy(() -> { diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java index b8a0aa9258e..b6648bea4f8 100644 --- a/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -161,9 +161,4 @@ public void setAuthenticationTrustResolver() { assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationTrustResolver(null)); } - @Test - public void setMessageSource() { - assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setMessageSource(null)); - } - } diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java new file mode 100644 index 00000000000..5b42423e2c9 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authorization; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IpAddressReactiveAuthorizationManager} + * + * @author Guirong Hu + */ +public class IpAddressReactiveAuthorizationManagerTests { + + @Test + public void checkWhenHasIpv6AddressThenReturnTrue() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v6manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("fe80::21f:5bff:fe33:bd68"); + boolean granted = v6manager.check(null, context("fe80::21f:5bff:fe33:bd68")).block().isGranted(); + assertThat(granted).isTrue(); + } + + @Test + public void checkWhenHasIpv6AddressThenReturnFalse() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v6manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("fe80::21f:5bff:fe33:bd68"); + boolean granted = v6manager.check(null, context("fe80::1c9a:7cfd:29a8:a91e")).block().isGranted(); + assertThat(granted).isFalse(); + } + + @Test + public void checkWhenHasIpv4AddressThenReturnTrue() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v4manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("192.168.1.104"); + boolean granted = v4manager.check(null, context("192.168.1.104")).block().isGranted(); + assertThat(granted).isTrue(); + } + + @Test + public void checkWhenHasIpv4AddressThenReturnFalse() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v4manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("192.168.1.104"); + boolean granted = v4manager.check(null, context("192.168.100.15")).block().isGranted(); + assertThat(granted).isFalse(); + } + + private static AuthorizationContext context(String ipAddress) throws UnknownHostException { + MockServerWebExchange exchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/") + .remoteAddress(new InetSocketAddress(InetAddress.getByName(ipAddress), 8080))).build(); + return new AuthorizationContext(exchange); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java index f4af6f74f2b..aa372e69fb6 100644 --- a/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java @@ -17,14 +17,19 @@ package org.springframework.security.web.server.context; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.publisher.PublisherProbe; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * @author Rob Winch @@ -79,4 +84,25 @@ public void loadWhenNullThenNull() { assertThat(context).isNull(); } + @Test + public void loadWhenCacheSecurityContextThenSubscribeOnce() { + PublisherProbe webSession = PublisherProbe.empty(); + ServerWebExchange exchange = mock(ServerWebExchange.class); + given(exchange.getSession()).willReturn(webSession.mono()); + this.repository.setCacheSecurityContext(true); + Mono context = this.repository.load(exchange); + assertThat(context.block()).isSameAs(context.block()); + assertThat(webSession.subscribeCount()).isEqualTo(1); + } + + @Test + public void loadWhenNotCacheSecurityContextThenSubscribeMultiple() { + PublisherProbe webSession = PublisherProbe.empty(); + ServerWebExchange exchange = mock(ServerWebExchange.class); + given(exchange.getSession()).willReturn(webSession.mono()); + Mono context = this.repository.load(exchange); + assertThat(context.block()).isSameAs(context.block()); + assertThat(webSession.subscribeCount()).isEqualTo(2); + } + } diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java index e31c239219e..aada7a4b629 100644 --- a/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java @@ -189,6 +189,17 @@ public void filterWhenMultipartFormDataAndEnabledThenGranted() { .expectStatus().is2xxSuccessful(); } + @Test + public void filterWhenPostAndMultipartFormDataEnabledAndNoBodyProvided() { + this.csrfFilter.setCsrfTokenRepository(this.repository); + this.csrfFilter.setTokenFromMultipartDataEnabled(true); + given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); + given(this.repository.generateToken(any())).willReturn(Mono.just(this.token)); + WebTestClient client = WebTestClient.bindToController(new OkController()).webFilter(this.csrfFilter).build(); + client.post().uri("/").header(this.token.getHeaderName(), this.token.getToken()).exchange().expectStatus() + .is2xxSuccessful(); + } + @Test public void filterWhenFormDataAndEnabledThenGranted() { this.csrfFilter.setCsrfTokenRepository(this.repository); diff --git a/web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java new file mode 100644 index 00000000000..b4e99336fc2 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginEmbedderPolicyServerHttpHeadersWriterTests { + + private ServerWebExchange exchange; + + private CrossOriginEmbedderPolicyServerHttpHeadersWriter writer; + + @BeforeEach + void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new CrossOriginEmbedderPolicyServerHttpHeadersWriter(); + } + + @Test + void setEmbedderPolicyWhenNullEmbedderPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("embedderPolicy cannot be null"); + } + + @Test + void writeHeadersWhenNoValuesThenDoesNotWriteHeaders() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.exchange.getResponse().getHeaders().add(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY, + "require-corp"); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY)) + .containsOnly("require-corp"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY)) + .containsOnly("require-corp"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java new file mode 100644 index 00000000000..0159665b4ef --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginOpenerPolicyServerHttpHeadersWriterTests { + + private ServerWebExchange exchange; + + private CrossOriginOpenerPolicyServerHttpHeadersWriter writer; + + @BeforeEach + void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new CrossOriginOpenerPolicyServerHttpHeadersWriter(); + } + + @Test + void setOpenerPolicyWhenNullOpenerPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("openerPolicy cannot be null"); + } + + @Test + void writeHeadersWhenNoValuesThenDoesNotWriteHeaders() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.exchange.getResponse().getHeaders().add(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY, + "same-origin"); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY)) + .containsOnly("same-origin"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy( + CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY)) + .containsOnly("same-origin-allow-popups"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java new file mode 100644 index 00000000000..a3ba9a2ec9f --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginResourcePolicyServerHttpHeadersWriterTests { + + private ServerWebExchange exchange; + + private CrossOriginResourcePolicyServerHttpHeadersWriter writer; + + @BeforeEach + void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new CrossOriginResourcePolicyServerHttpHeadersWriter(); + } + + @Test + void setResourcePolicyWhenNullThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("resourcePolicy cannot be null"); + } + + @Test + void writeHeadersWhenNoValuesThenDoesNotWriteHeaders() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.exchange.getResponse().getHeaders().add(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY, + "same-origin"); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY)) + .containsOnly("same-origin"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY)) + .containsOnly("same-origin"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java new file mode 100644 index 00000000000..c3c38ff0a32 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author David Herberth + */ +@ExtendWith(MockitoExtension.class) +public class ServerWebExchangeDelegatingServerHttpHeadersWriterTests { + + @Mock + private ServerWebExchangeMatcher matcher; + + @Mock + private ServerHttpHeadersWriter delegate; + + private ServerWebExchange exchange; + + private ServerWebExchangeDelegatingServerHttpHeadersWriter headerWriter; + + @BeforeEach + public void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.headerWriter = new ServerWebExchangeDelegatingServerHttpHeadersWriter(this.matcher, this.delegate); + } + + @Test + public void constructorWhenNullWebExchangeMatcherThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter(null, this.delegate)) + .withMessage("webExchangeMatcher cannot be null"); + } + + @Test + public void constructorWhenNullWebExchangeMatcherEntryThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter(null)) + .withMessage("headersWriter cannot be null"); + } + + @Test + public void constructorWhenNullDelegateHeadersWriterThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter(this.matcher, null)) + .withMessage("delegateHeadersWriter cannot be null"); + } + + @Test + public void constructorWhenEntryWithNullMatcherThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter( + new ServerWebExchangeMatcherEntry<>(null, this.delegate))) + .withMessage("webExchangeMatcher cannot be null"); + } + + @Test + public void constructorWhenEntryWithNullEntryThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter( + new ServerWebExchangeMatcherEntry<>(this.matcher, null))) + .withMessage("delegateHeadersWriter cannot be null"); + } + + @Test + public void writeHeadersWhenMatchThenDelegateWriteHttpHeaders() { + given(this.matcher.matches(this.exchange)) + .willReturn(ServerWebExchangeMatcher.MatchResult.match(Collections.emptyMap())); + given(this.delegate.writeHttpHeaders(this.exchange)).willReturn(Mono.empty()); + this.headerWriter.writeHttpHeaders(this.exchange).block(); + verify(this.delegate).writeHttpHeaders(this.exchange); + } + + @Test + public void writeHeadersWhenNoMatchThenDelegateNotCalled() { + given(this.matcher.matches(this.exchange)).willReturn(ServerWebExchangeMatcher.MatchResult.notMatch()); + this.headerWriter.writeHttpHeaders(this.exchange).block(); + verify(this.matcher).matches(this.exchange); + verify(this.delegate, times(0)).writeHttpHeaders(this.exchange); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java index e411ee745bc..604d20d56db 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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,11 +16,14 @@ package org.springframework.security.web.server.header; +import java.util.Locale; + import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; @@ -56,6 +59,24 @@ public void writeHeadersWhenSingleHeaderAndHeaderWrittenThenSuccess() { .containsOnly(headerValue); } + // gh-10557 + @Test + public void writeHeadersWhenHeaderWrittenWithDifferentCaseThenDoesNotWriteHeaders() { + String headerName = HttpHeaders.CACHE_CONTROL.toLowerCase(Locale.ROOT); + String headerValue = "max-age=120"; + this.headers.set(headerName, headerValue); + // Note: This test inverts which collection uses case sensitive headers, + // due to the fact that gh-10557 reports NettyHeadersAdapter as the + // response headers implementation, which is not accessible here. + HttpHeaders caseSensitiveHeaders = new HttpHeaders(new LinkedMultiValueMap<>()); + caseSensitiveHeaders.set(HttpHeaders.CACHE_CONTROL, CacheControlServerHttpHeadersWriter.CACHE_CONTRTOL_VALUE); + caseSensitiveHeaders.set(HttpHeaders.PRAGMA, CacheControlServerHttpHeadersWriter.PRAGMA_VALUE); + caseSensitiveHeaders.set(HttpHeaders.EXPIRES, CacheControlServerHttpHeadersWriter.EXPIRES_VALUE); + this.writer = new StaticServerHttpHeadersWriter(caseSensitiveHeaders); + this.writer.writeHttpHeaders(this.exchange); + assertThat(this.headers.get(headerName)).containsOnly(headerValue); + } + @Test public void writeHeadersWhenMultiHeaderThenWritesAllHeaders() { this.writer = StaticServerHttpHeadersWriter.builder() diff --git a/web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java new file mode 100644 index 00000000000..3c26dfdfd93 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.util.matcher; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link IpAddressServerWebExchangeMatcher} + * + * @author Guirong Hu + */ +@ExtendWith(MockitoExtension.class) +public class IpAddressServerWebExchangeMatcherTests { + + @Test + public void matchesWhenIpv6RangeAndIpv6AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv6Exchange = exchange("fe80::21f:5bff:fe33:bd68"); + ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("fe80::21f:5bff:fe33:bd68") + .matches(ipv6Exchange).block(); + assertThat(matches.isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv6RangeAndIpv4AddressThenFalse() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("fe80::21f:5bff:fe33:bd68") + .matches(ipv4Exchange).block(); + assertThat(matches.isMatch()).isFalse(); + } + + @Test + public void matchesWhenIpv4RangeAndIpv4AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("192.168.1.104") + .matches(ipv4Exchange).block(); + assertThat(matches.isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv4SubnetAndIpv4AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("192.168.1.0/24"); + assertThat(matcher.matches(ipv4Exchange).block().isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv4SubnetAndIpv4AddressThenFalse() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("192.168.1.128/25"); + assertThat(matcher.matches(ipv4Exchange).block().isMatch()).isFalse(); + } + + @Test + public void matchesWhenIpv6SubnetAndIpv6AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv6Exchange = exchange("2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("2001:DB8::/48"); + assertThat(matcher.matches(ipv6Exchange).block().isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv6SubnetAndIpv6AddressThenFalse() throws UnknownHostException { + ServerWebExchange ipv6Exchange = exchange("2001:DB8:1:0:0:0:0:0"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("2001:DB8::/48"); + assertThat(matcher.matches(ipv6Exchange).block().isMatch()).isFalse(); + } + + @Test + public void matchesWhenZeroMaskAndAnythingThenTrue() throws UnknownHostException { + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("0.0.0.0/0"); + assertThat(matcher.matches(exchange("123.4.5.6")).block().isMatch()).isTrue(); + assertThat(matcher.matches(exchange("192.168.0.159")).block().isMatch()).isTrue(); + matcher = new IpAddressServerWebExchangeMatcher("192.168.0.159/0"); + assertThat(matcher.matches(exchange("123.4.5.6")).block().isMatch()).isTrue(); + assertThat(matcher.matches(exchange("192.168.0.159")).block().isMatch()).isTrue(); + } + + @Test + public void constructorWhenIpv4AddressMaskTooLongThenIllegalArgumentException() { + String ipv4AddressWithTooLongMask = "192.168.1.104/33"; + assertThatIllegalArgumentException() + .isThrownBy(() -> new IpAddressServerWebExchangeMatcher(ipv4AddressWithTooLongMask)) + .withMessage(String.format("IP address %s is too short for bitmask of length %d", "192.168.1.104", 33)); + } + + @Test + public void constructorWhenIpv6AddressMaskTooLongThenIllegalArgumentException() { + String ipv6AddressWithTooLongMask = "fe80::21f:5bff:fe33:bd68/129"; + assertThatIllegalArgumentException() + .isThrownBy(() -> new IpAddressServerWebExchangeMatcher(ipv6AddressWithTooLongMask)) + .withMessage(String.format("IP address %s is too short for bitmask of length %d", + "fe80::21f:5bff:fe33:bd68", 129)); + } + + private static ServerWebExchange exchange(String ipAddress) throws UnknownHostException { + return MockServerWebExchange.builder(MockServerHttpRequest.get("/") + .remoteAddress(new InetSocketAddress(InetAddress.getByName(ipAddress), 8080))).build(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java index d7c2d9d77dd..bfb0d965115 100644 --- a/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.server.util.matcher; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -43,6 +45,21 @@ public void constructorMediaTypeArrayWhenNullThenThrowsIllegalArgumentException( assertThatIllegalArgumentException().isThrownBy(() -> new MediaTypeServerWebExchangeMatcher(types)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointerException() { + List mediaTypes = new ArrayList(Arrays.asList(MediaType.ALL)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new MediaTypeServerWebExchangeMatcher(mediaTypes); + } + @Test public void constructorMediaTypeArrayWhenContainsNullThenThrowsIllegalArgumentException() { MediaType[] types = { null }; diff --git a/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java b/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java index 5e81db0a107..78352903c1f 100644 --- a/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * Copyright 2004, 2005, 2006, 2021 Acegi Technology Pty Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -45,12 +46,14 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -217,6 +220,27 @@ public void loginNullAuthenticationManagerFail() throws Exception { verifyZeroInteractions(this.authenticationEntryPoint, this.authenticationManager, this.logoutHandler); } + @Test + public void loginWhenHttpServletRequestHasAuthenticationDetailsThenAuthenticationRequestHasDetails() + throws Exception { + String ipAddress = "10.0.0.100"; + String sessionId = "session-id"; + given(this.request.getRemoteAddr()).willReturn(ipAddress); + given(this.request.getSession(anyBoolean())).willReturn(new MockHttpSession(null, sessionId)); + wrappedRequest().login("username", "password"); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor + .forClass(UsernamePasswordAuthenticationToken.class); + verify(this.authenticationManager).authenticate(authenticationCaptor.capture()); + + UsernamePasswordAuthenticationToken authenticationRequest = authenticationCaptor.getValue(); + assertThat(authenticationRequest.getDetails()).isInstanceOf(WebAuthenticationDetails.class); + + WebAuthenticationDetails details = (WebAuthenticationDetails) authenticationRequest.getDetails(); + assertThat(details.getRemoteAddress()).isEqualTo(ipAddress); + assertThat(details.getSessionId()).isEqualTo(sessionId); + } + @Test public void logout() throws Exception { TestingAuthenticationToken expectedAuth = new TestingAuthenticationToken("user", "password", "ROLE_USER"); diff --git a/web/src/test/java/org/springframework/security/web/session/DisableEncodeUrlFilterTests.java b/web/src/test/java/org/springframework/security/web/session/DisableEncodeUrlFilterTests.java new file mode 100644 index 00000000000..cb33a74b4df --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/session/DisableEncodeUrlFilterTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.session; + +import java.util.function.Consumer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * @author Rob Winch + */ +@ExtendWith(MockitoExtension.class) +class DisableEncodeUrlFilterTests { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private DisableEncodeUrlFilter filter = new DisableEncodeUrlFilter(); + + @Test + void doFilterDisablesEncodeURL() throws Exception { + verifyDoFilterDoesNotInteractWithResponse((httpResponse) -> httpResponse.encodeURL("/")); + } + + @Test + void doFilterDisablesEncodeUrl() throws Exception { + verifyDoFilterDoesNotInteractWithResponse((httpResponse) -> httpResponse.encodeUrl("/")); + } + + @Test + void doFilterDisablesEncodeRedirectURL() throws Exception { + verifyDoFilterDoesNotInteractWithResponse((httpResponse) -> httpResponse.encodeRedirectURL("/")); + } + + @Test + void doFilterDisablesEncodeRedirectUrl() throws Exception { + verifyDoFilterDoesNotInteractWithResponse((httpResponse) -> httpResponse.encodeRedirectUrl("/")); + } + + private void verifyDoFilterDoesNotInteractWithResponse(Consumer toInvoke) throws Exception { + this.filter.doFilter(this.request, this.response, (request, response) -> { + HttpServletResponse httpResponse = (HttpServletResponse) response; + toInvoke.accept(httpResponse); + }); + verifyNoInteractions(this.response); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/session/ForceEagerSessionCreationFilterTests.java b/web/src/test/java/org/springframework/security/web/session/ForceEagerSessionCreationFilterTests.java new file mode 100644 index 00000000000..33597b5e5c8 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/session/ForceEagerSessionCreationFilterTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.session; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class ForceEagerSessionCreationFilterTests { + + @Test + void createsSession() throws Exception { + ForceEagerSessionCreationFilter filter = new ForceEagerSessionCreationFilter(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockFilterChain chain = new MockFilterChain(); + + filter.doFilter(request, new MockHttpServletResponse(), chain); + + assertThat(request.getSession(false)).isNotNull(); + assertThat(chain.getRequest()).isEqualTo(request); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java index a53e569c18a..99b641580b0 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.util.matcher; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -55,6 +56,22 @@ public void constructorNullArray() { assertThatNullPointerException().isThrownBy(() -> new AndRequestMatcher((RequestMatcher[]) null)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointer() { + List requestMatchers = new ArrayList( + Arrays.asList(AnyRequestMatcher.INSTANCE)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new AndRequestMatcher(requestMatchers); + } + @Test public void constructorArrayContainsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new AndRequestMatcher((RequestMatcher) null)); diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java index 37314e174c6..02f41fbc2fc 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.util.matcher; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -55,6 +56,23 @@ public void constructorNullArray() { assertThatNullPointerException().isThrownBy(() -> new OrRequestMatcher((RequestMatcher[]) null)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointer() { + // emulate List.of for pre-JDK 9 builds + List requestMatchers = new ArrayList( + Arrays.asList(AnyRequestMatcher.INSTANCE)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new OrRequestMatcher(requestMatchers); + } + @Test public void constructorArrayContainsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new OrRequestMatcher((RequestMatcher) null)); diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java index 3a87bdc5f94..66f0a3d6416 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java @@ -101,6 +101,22 @@ public void matchesWithInvalidMethod() { assertThat(matcher.matches(request)).isFalse(); } + @Test + public void matchesWithCarriageReturn() { + RegexRequestMatcher matcher = new RegexRequestMatcher(".*", null); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/blah%0a"); + request.setServletPath("/blah\n"); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void matchesWithLineFeed() { + RegexRequestMatcher matcher = new RegexRequestMatcher(".*", null); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/blah%0d"); + request.setServletPath("/blah\r"); + assertThat(matcher.matches(request)).isTrue(); + } + @Test public void toStringThenFormatted() { RegexRequestMatcher matcher = new RegexRequestMatcher("/blah", "GET"); diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java new file mode 100644 index 00000000000..b293b68caaa --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.util.matcher; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class RequestMatcherEntryTests { + + @Test + void constructWhenGetRequestMatcherAndEntryThenSameRequestMatcherAndEntry() { + RequestMatcher requestMatcher = mock(RequestMatcher.class); + RequestMatcherEntry entry = new RequestMatcherEntry<>(requestMatcher, "entry"); + assertThat(entry.getRequestMatcher()).isSameAs(requestMatcher); + assertThat(entry.getEntry()).isEqualTo("entry"); + } + + @Test + void constructWhenNullValuesThenNullValues() { + RequestMatcherEntry entry = new RequestMatcherEntry<>(null, null); + assertThat(entry.getRequestMatcher()).isNull(); + assertThat(entry.getEntry()).isNull(); + } + +}