diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index f7f680f56c7..cc4f6c94f90 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -41,29 +41,29 @@ jobs: java-version: ${{ matrix.java-version }} test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.2.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2024.0.+ --stacktrace secrets: inherit -# check-samples: -# name: Check Samples -# runs-on: ubuntu-latest -# if: ${{ github.repository_owner == 'spring-projects' }} -# steps: -# - uses: actions/checkout@v4 -# - name: Set up gradle -# uses: spring-io/spring-gradle-build-action@v2 -# with: -# java-version: 17 -# distribution: temurin -# - name: Check samples project -# env: -# LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos -# SAMPLES_DIR: ../spring-security-samples -# run: | -# # Extract version from gradle.properties -# version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') -# # Extract samplesBranch from gradle.properties -# samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}') -# ./gradlew publishMavenJavaPublicationToLocalRepository -# ./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR" -# ./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" :runAllTests + check-samples: + name: Check Samples + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'spring-projects' }} + steps: + - uses: actions/checkout@v4 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v2 + with: + java-version: 17 + distribution: temurin + - name: Check samples project + env: + LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos + SAMPLES_DIR: ../spring-security-samples + run: | + # Extract version from gradle.properties + version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') + # Extract samplesBranch from gradle.properties + samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}') + ./gradlew publishMavenJavaPublicationToLocalRepository + ./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR" + ./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" check check-tangles: name: Check for Package Tangles runs-on: ubuntu-latest @@ -82,21 +82,21 @@ jobs: ./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace deploy-artifacts: name: Deploy Artifacts - needs: [ build, test, check-tangles ] + needs: [ build, test, check-samples, check-tangles ] uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1 with: should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }} secrets: inherit deploy-docs: name: Deploy Docs - needs: [ build, test, check-tangles ] + needs: [ build, test, check-samples, check-tangles ] uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1 with: should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }} secrets: inherit deploy-schema: name: Deploy Schema - needs: [ build, test, check-tangles ] + needs: [ build, test, check-samples, check-tangles ] uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1 with: should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }} diff --git a/.github/workflows/mark-duplicate-dependabot-prs.yml b/.github/workflows/mark-duplicate-dependabot-prs.yml new file mode 100644 index 00000000000..7d1574309d9 --- /dev/null +++ b/.github/workflows/mark-duplicate-dependabot-prs.yml @@ -0,0 +1,46 @@ +name: Mark Duplicate Dependabot PRs + +on: + pull_request: + types: [closed] + +jobs: + check_duplicate_prs: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Extract Dependency Name from PR Title + id: extract + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}') + echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT + + - name: Find PRs + id: find_duplicates + env: + DEPENDENCY_NAME: ${{ steps.extract.outputs.dependency_name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "$DEPENDENCY_NAME"' --json number --jq 'map(.number) | join(",")') + echo "prs=$PRS" >> $GITHUB_OUTPUT + + - name: Label Duplicate PRs + if: steps.find_duplicates.outputs.prs != '' + env: + PRS: ${{ steps.find_duplicates.outputs.prs }} + CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + for i in ${PRS//,/ } + do + if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then + echo "Marking PR $i as duplicate" + gh pr edit "$i" --add-label "status: duplicate" + gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER" + fi + done diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index 22bc74e95ef..38ba42600b9 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -21,7 +21,7 @@ jobs: java-version: '17' distribution: 'temurin' - name: Build with Gradle - run: ./gradlew clean build --continue + run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue generate-docs: name: Generate Docs runs-on: ubuntu-latest diff --git a/README.adoc b/README.adoc index 73d95c29f40..37fa14d836d 100644 --- a/README.adoc +++ b/README.adoc @@ -18,7 +18,7 @@ Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_COND See https://docs.spring.io/spring-security/reference/getting-spring-security.html[Getting Spring Security] for how to obtain Spring Security. == Documentation -Be sure to read the https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/[Spring Security Reference]. +Be sure to read the https://docs.spring.io/spring-security/reference/[Spring Security Reference]. Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation]. == Quick Start diff --git a/build.gradle b/build.gradle index 952f5b2fef2..47e75069084 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ apply plugin: 's101' apply plugin: 'io.spring.convention.root' apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'org.springframework.security.versions.verify-dependencies-versions' +apply plugin: 'org.springframework.security.check-expected-branch-version' apply plugin: 'io.spring.security.release' group = 'org.springframework.security' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 7cf23c305d1..ab58495e72e 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -47,6 +47,10 @@ gradlePlugin { id = "org.springframework.security.versions.verify-dependencies-versions" implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin" } + checkExpectedBranchVersion { + id = "org.springframework.security.check-expected-branch-version" + implementationClass = "org.springframework.security.CheckExpectedBranchVersionPlugin" + } } } diff --git a/buildSrc/src/main/java/org/springframework/security/CheckExpectedBranchVersionPlugin.java b/buildSrc/src/main/java/org/springframework/security/CheckExpectedBranchVersionPlugin.java new file mode 100644 index 00000000000..9266f7c4d5b --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/CheckExpectedBranchVersionPlugin.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; + +/** + * @author Marcus da Coregio + */ +public class CheckExpectedBranchVersionPlugin implements Plugin { + + @Override + public void apply(Project project) { + TaskProvider checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> { + task.setGroup("Build"); + task.setDescription("Check if the project version matches the branch version"); + }); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask)); + } + + public static class CheckExpectedBranchVersionTask extends DefaultTask { + + @TaskAction + public void run() throws IOException { + Project project = getProject(); + if (project.hasProperty("skipCheckExpectedBranchVersion")) { + return; + } + String version = (String) project.getVersion(); + String branchVersion = getBranchVersion(project); + if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) { + System.out.println("Branch version does not match *.x, ignoring"); + return; + } + if (!versionsMatch(version, branchVersion)) { + throw new IllegalStateException(String.format("Project version [%s] does not match branch version [%s]. " + + "Please verify that the branch contains the right version.", version, branchVersion)); + } + } + + private static String getBranchVersion(Project project) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + project.exec((exec) -> { + exec.commandLine("git", "symbolic-ref", "--short", "HEAD"); + exec.setErrorOutput(System.err); + exec.setStandardOutput(baos); + }); + return baos.toString(); + } + } + + private boolean versionsMatch(String projectVersion, String branchVersion) { + String[] projectVersionParts = projectVersion.split("\\."); + String[] branchVersionParts = branchVersion.split("\\."); + if (projectVersionParts.length < 2 || branchVersionParts.length < 2) { + return false; + } + return projectVersionParts[0].equals(branchVersionParts[0]) && projectVersionParts[1].equals(branchVersionParts[1]); + } + + } + +} 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 914d46f8bae..960e9e1d71c 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.function.Predicate; import jakarta.servlet.http.HttpServletRequest; @@ -60,6 +58,7 @@ import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.ParameterRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; /** @@ -508,23 +507,6 @@ public boolean matches(HttpServletRequest request) { } - 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 Saml2RelyingPartyInitiatedLogoutFilter extends LogoutFilter { Saml2RelyingPartyInitiatedLogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) { 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 index 5f894cf8d8f..860ed9fc551 100644 --- a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.function.Predicate; import jakarta.servlet.http.HttpServletRequest; import org.w3c.dom.Element; @@ -44,6 +42,7 @@ 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.ParameterRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -228,23 +227,6 @@ 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)); - } - - } - public static class Saml2RequestMatcher implements RequestMatcher { private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder 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 e34b45a33cb..00c70003bcc 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 @@ -82,9 +82,8 @@ public void beforeWhenMockAuthorizationManagerThenCheckAndReturnedObject() throw @Test public void afterWhenMockSecurityContextHolderStrategyThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); Authentication authentication = TestAuthentication.authenticatedUser(); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); MethodInvocation invocation = mock(MethodInvocation.class); AuthorizationManager authorizationManager = AuthenticatedAuthorizationManager .authenticated(); @@ -98,10 +97,9 @@ public void afterWhenMockSecurityContextHolderStrategyThenUses() throws Throwabl // gh-12877 @Test public void afterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); Authentication authentication = new TestingAuthenticationToken("john", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); MethodInvocation invocation = mock(MethodInvocation.class); AuthorizationManager authorizationManager = AuthenticatedAuthorizationManager .authenticated(); 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 8022609ac7b..a39b1853980 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 @@ -77,10 +77,9 @@ public void beforeWhenMockAuthorizationManagerThenCheck() throws Throwable { @Test public void beforeWhenMockSecurityContextHolderStrategyThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); Authentication authentication = new TestingAuthenticationToken("user", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); MethodInvocation invocation = mock(MethodInvocation.class); AuthorizationManager authorizationManager = AuthenticatedAuthorizationManager.authenticated(); AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor( @@ -93,10 +92,10 @@ public void beforeWhenMockSecurityContextHolderStrategyThenUses() throws Throwab // gh-12877 @Test public void beforeWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); + Authentication authentication = new TestingAuthenticationToken("john", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); MethodInvocation invocation = mock(MethodInvocation.class); AuthorizationManager authorizationManager = AuthenticatedAuthorizationManager.authenticated(); AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor( diff --git a/core/src/test/java/org/springframework/security/authorization/method/MockSecurityContextHolderStrategy.java b/core/src/test/java/org/springframework/security/authorization/method/MockSecurityContextHolderStrategy.java new file mode 100644 index 00000000000..eec416925e3 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/MockSecurityContextHolderStrategy.java @@ -0,0 +1,15 @@ +package org.springframework.security.authorization.method; + +import org.springframework.security.core.context.*; + +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.*; + +public class MockSecurityContextHolderStrategy { + static SecurityContextHolderStrategy getmock(SecurityContextImpl securityContextImpl){ + + SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); + given(strategy.getContext()).willReturn(securityContextImpl); + return strategy; + } +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java index 48eec006186..8428ab1ec59 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java @@ -129,10 +129,10 @@ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationE @Test public void postFilterWhenMockSecurityContextHolderStrategyThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); + Authentication authentication = new TestingAuthenticationToken("john", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); String[] array = { "john", "bob" }; MockMethodInvocation invocation = new MockMethodInvocation(new TestClass(), TestClass.class, "doSomethingArrayAuthentication", new Class[] { String[].class }, new Object[] { array }) { @@ -150,10 +150,10 @@ public Object proceed() { // gh-12877 @Test public void postFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); + Authentication authentication = new TestingAuthenticationToken("john", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); String[] array = { "john", "bob" }; MockMethodInvocation invocation = new MockMethodInvocation(new TestClass(), TestClass.class, "doSomethingArrayAuthentication", new Class[] { String[].class }, new Object[] { array }) { diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java index 30d40a369fe..e4d3e74d433 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java @@ -189,10 +189,9 @@ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationE @Test public void preFilterWhenMockSecurityContextHolderStrategyThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); Authentication authentication = new TestingAuthenticationToken("john", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); List list = new ArrayList<>(); list.add("john"); list.add("bob"); @@ -207,10 +206,9 @@ public void preFilterWhenMockSecurityContextHolderStrategyThenUses() throws Thro // gh-12877 @Test public void preFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable { - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); Authentication authentication = new TestingAuthenticationToken("john", "password", AuthorityUtils.createAuthorityList("authority")); - given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication)); + SecurityContextHolderStrategy strategy = MockSecurityContextHolderStrategy.getmock(new SecurityContextImpl(authentication)); List list = new ArrayList<>(); list.add("john"); list.add("bob"); diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml index dbc3514c70f..78a6605fae3 100644 --- a/docs/antora-playbook.yml +++ b/docs/antora-playbook.yml @@ -24,6 +24,7 @@ asciidoc: extensions: - '@asciidoctor/tabs' - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/javadoc-extension' urls: latest_version_segment_strategy: redirect:to latest_version_segment: '' diff --git a/docs/antora.yml b/docs/antora.yml index 084191a21ee..571640b91d5 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -6,7 +6,7 @@ nav: ext: collector: run: - command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntoraYml + command: gradlew -q -PbuildSrc.skipTests=true :spring-security-docs:generateAntoraResources local: true scan: dir: ./build/generated-antora-resources diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index edc2ce6aaf5..7ba36aa76b5 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -8,6 +8,7 @@ * xref:migration/index.adoc[Migrating to 6.2] ** xref:migration/authorization.adoc[Authorization Changes] * xref:getting-spring-security.adoc[Getting Spring Security] +* xref:attachment$api/java/index.html[Javadoc] * xref:features/index.adoc[Features] ** xref:features/authentication/index.adoc[Authentication] *** xref:features/authentication/password-storage.adoc[Password Storage] @@ -38,6 +39,7 @@ ***** xref:servlet/authentication/passwords/in-memory.adoc[In Memory] ***** xref:servlet/authentication/passwords/jdbc.adoc[JDBC] ***** xref:servlet/authentication/passwords/user-details.adoc[UserDetails] +***** xref:servlet/authentication/passwords/credentials-container.adoc[CredentialsContainer] ***** xref:servlet/authentication/passwords/user-details-service.adoc[UserDetailsService] ***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder] ***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider] diff --git a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc index c10967b7c44..b6d217dd854 100644 --- a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc +++ b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc @@ -598,11 +598,15 @@ With the above configuration, when a password manager navigates to `/.well-known There are some scenarios where you need to check whether a password has been compromised, for example, if you are creating an application that deals with sensitive data, it is often needed that you perform some check on user's passwords in order to assert its reliability. One of these checks can be if the password has been compromised, usually because it has been found in a https://wikipedia.org/wiki/Data_breach[data breach]. -To facilitate that, Spring Security provides integration with the https://haveibeenpwned.com/API/v3#PwnedPasswords[Have I Been Pwned API] via the {security-api-url}org/springframework/security/core/password/HaveIBeenPwnedRestApiPasswordChecker.html[`HaveIBeenPwnedRestApiPasswordChecker` implementation] of the {security-api-url}org/springframework/security/core/password/CompromisedPasswordChecker.html[`CompromisedPasswordChecker` interface]. +To facilitate that, Spring Security provides integration with the https://haveibeenpwned.com/API/v3#PwnedPasswords[Have I Been Pwned API] via the javadoc:org.springframework.security.web.authentication.password.HaveIBeenPwnedRestApiPasswordChecker[] implementation of the javadoc:org.springframework.security.authentication.password.CompromisedPasswordChecker[] interface. You can either use the `CompromisedPasswordChecker` API by yourself or, if you are using xref:servlet/authentication/passwords/dao-authentication-provider.adoc[the `DaoAuthenticationProvider]` via xref:servlet/authentication/passwords/index.adoc[Spring Security authentication mechanisms], you can provide a `CompromisedPasswordChecker` bean, and it will be automatically picked up by Spring Security configuration. -.Using CompromisedPasswordChecker as a bean +By doing that, when you try to authenticate via Form Login using a weak password, let's say `123456`, you will receive a 401 or be redirected to the `/login?error` page (depending on your user-agent). +However, just a 401 or the redirect is not so useful in that case, it will cause some confusion because the user provided the right password and still was not allowed to log in. +In such cases, you can handle the `CompromisedPasswordException` via the `AuthenticationFailureHandler` to perform your desired logic, like redirecting the user-agent to `/reset-password`, for example: + +.Using CompromisedPasswordChecker [tabs] ====== Java:: @@ -615,8 +619,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) - .formLogin(withDefaults()) - .httpBasic(withDefaults()); + .formLogin((login) -> login + .failureHandler(new CompromisedPasswordAuthenticationFailureHandler()) + ); return http.build(); } @@ -624,6 +629,25 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CompromisedPasswordChecker compromisedPasswordChecker() { return new HaveIBeenPwnedRestApiPasswordChecker(); } + +static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler( + "/login?error"); + + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + if (exception instanceof CompromisedPasswordException) { + this.redirectStrategy.sendRedirect(request, response, "/reset-password"); + return; + } + this.defaultFailureHandler.onAuthenticationFailure(request, response, exception); + } + +} ---- Kotlin:: @@ -636,8 +660,9 @@ open fun filterChain(http:HttpSecurity): SecurityFilterChain { authorizeHttpRequests { authorize(anyRequest, authenticated) } - formLogin {} - httpBasic {} + formLogin { + failureHandler = CompromisedPasswordAuthenticationFailureHandler() + } } return http.build() } @@ -646,44 +671,22 @@ open fun filterChain(http:HttpSecurity): SecurityFilterChain { open fun compromisedPasswordChecker(): CompromisedPasswordChecker { return HaveIBeenPwnedRestApiPasswordChecker() } ----- -====== - -By doing that, when you try to authenticate via HTTP Basic or Form Login using a weak password, let's say `123456`, you will receive a 401 response status code. -However, just a 401 is not so useful in that case, it will cause some confusion because the user provided the right password and still was not allowed to log in. -In such cases, you can handle the `CompromisedPasswordException` to perform your desired logic, like redirecting the user-agent to `/reset-password`, for example: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@ControllerAdvice -public class MyControllerAdvice { - - @ExceptionHandler(CompromisedPasswordException.class) - public String handleCompromisedPasswordException(CompromisedPasswordException ex, RedirectAttributes attributes) { - attributes.addFlashAttribute("error", ex.message); - return "redirect:/reset-password"; - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@ControllerAdvice -class MyControllerAdvice { - @ExceptionHandler(CompromisedPasswordException::class) - fun handleCompromisedPasswordException(ex: CompromisedPasswordException, attributes: RedirectAttributes): RedirectView { - attributes.addFlashAttribute("error", ex.message) - return RedirectView("/reset-password") +class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler { + private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error") + private val redirectStrategy = DefaultRedirectStrategy() + + override fun onAuthenticationFailure( + request: HttpServletRequest, + response: HttpServletResponse, + exception: AuthenticationException + ) { + if (exception is CompromisedPasswordException) { + redirectStrategy.sendRedirect(request, response, "/reset-password") + return + } + defaultFailureHandler.onAuthenticationFailure(request, response, exception) } - } ---- ====== diff --git a/docs/modules/ROOT/pages/features/integrations/cryptography.adoc b/docs/modules/ROOT/pages/features/integrations/cryptography.adoc index 01c78af0bc2..01c71c36f08 100644 --- a/docs/modules/ROOT/pages/features/integrations/cryptography.adoc +++ b/docs/modules/ROOT/pages/features/integrations/cryptography.adoc @@ -8,9 +8,9 @@ The code is distributed as part of the core module but has no dependencies on an [[spring-security-crypto-encryption]] == Encryptors -The {security-api-url}org/springframework/security/crypto/encrypt/Encryptors.html[`Encryptors`] class provides factory methods for constructing symmetric encryptors. -This class lets you create {security-api-url}org/springframework/security/crypto/encrypt/BytesEncryptor.html[`BytesEncryptor`] instances to encrypt data in raw `byte[]` form. -You can also construct {security-api-url}org/springframework/security/crypto/encrypt/TextEncryptor.html[TextEncryptor] instances to encrypt text strings. +The javadoc:org.springframework.security.crypto.encrypt.Encryptors[] class provides factory methods for constructing symmetric encryptors. +This class lets you create javadoc:org.springframework.security.crypto.encrypt.BytesEncryptor[] instances to encrypt data in raw `byte[]` form. +You can also construct javadoc:org.springframework.security.crypto.encrypt.TextEncryptor[] instances to encrypt text strings. Encryptors are thread-safe. [NOTE] @@ -101,9 +101,9 @@ Encrypted results are returned as hex-encoded strings for easy storage on the fi [[spring-security-crypto-keygenerators]] == Key Generators -The {security-api-url}org/springframework/security/crypto/keygen/KeyGenerators.html[`KeyGenerators`] class provides a number of convenience factory methods for constructing different types of key generators. -By using this class, you can create a {security-api-url}org/springframework/security/crypto/keygen/BytesKeyGenerator.html[`BytesKeyGenerator`] to generate `byte[]` keys. -You can also construct a {security-api-url}org/springframework/security/crypto/keygen/StringKeyGenerator.html`[StringKeyGenerator]` to generate string keys. +The javadoc:org.springframework.security.crypto.keygen.KeyGenerators[] class provides a number of convenience factory methods for constructing different types of key generators. +By using this class, you can create a javadoc:org.springframework.security.crypto.keygen.BytesKeyGenerator[] to generate `byte[]` keys. +You can also construct a javadoc:org.springframework.security.crypto.keygen.StringKeyGenerator[] to generate string keys. `KeyGenerators` is a thread-safe class. === BytesKeyGenerator diff --git a/docs/modules/ROOT/pages/prerequisites.adoc b/docs/modules/ROOT/pages/prerequisites.adoc index 641271d465f..b1e3993c730 100644 --- a/docs/modules/ROOT/pages/prerequisites.adoc +++ b/docs/modules/ROOT/pages/prerequisites.adoc @@ -1,7 +1,7 @@ [[prerequisites]] = Prerequisites -Spring Security requires a Java 8 or higher Runtime Environment. +Spring Security requires a Java 17 or higher Runtime Environment. As Spring Security aims to operate in a self-contained manner, you do not need to place any special configuration files in your Java Runtime Environment. In particular, you need not configure a special Java Authentication and Authorization Service (JAAS) policy file or place Spring Security into common classpath locations. diff --git a/docs/modules/ROOT/pages/reactive/authentication/concurrent-sessions-control.adoc b/docs/modules/ROOT/pages/reactive/authentication/concurrent-sessions-control.adoc index 7490c354fc4..3e4e436d34a 100644 --- a/docs/modules/ROOT/pages/reactive/authentication/concurrent-sessions-control.adoc +++ b/docs/modules/ROOT/pages/reactive/authentication/concurrent-sessions-control.adoc @@ -4,7 +4,7 @@ Similar to xref:servlet/authentication/session-management.adoc#ns-concurrent-sessions[Servlet's Concurrent Sessions Control], Spring Security also provides support to limit the number of concurrent sessions a user can have in a Reactive application. When you set up Concurrent Sessions Control in Spring Security, it monitors authentications carried out through Form Login, xref:reactive/oauth2/login/index.adoc[OAuth 2.0 Login], and HTTP Basic authentication by hooking into the way those authentication mechanisms handle authentication success. -More specifically, the session management DSL will add the {security-api-url}org/springframework/security/web/server/authentication/ConcurrentSessionControlServerAuthenticationSuccessHandler.html[ConcurrentSessionControlServerAuthenticationSuccessHandler] and the {security-api-url}org/springframework/security/web/server/authentication/RegisterSessionServerAuthenticationSuccessHandler.html[RegisterSessionServerAuthenticationSuccessHandler] to the list of `ServerAuthenticationSuccessHandler` used by the authentication filter. +More specifically, the session management DSL will add the javadoc:org.springframework.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler[] and the javadoc:org.springframework.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler[] to the list of `ServerAuthenticationSuccessHandler` used by the authentication filter. The following sections contains examples of how to configure Concurrent Sessions Control. @@ -197,9 +197,9 @@ If you also need to invalidate the session against the Identity Provider you mus [[concurrent-sessions-control-custom-strategy]] == Handling Maximum Number of Sessions Exceeded -By default, when the maximum number of sessions is exceeded, the least recently used session(s) will be expired by using the {security-api-url}org/springframework/security/web/server/authentication/session/InvalidateLeastUsedMaximumSessionsExceededHandler.html[InvalidateLeastUsedMaximumSessionsExceededHandler]. -Spring Security also provides another implementation that prevents the user from creating new sessions by using the {security-api-url}org/springframework/security/web/server/authentication/session/PreventLoginMaximumSessionsExceededHandler.html[PreventLoginMaximumSessionsExceededHandler]. -If you want to use your own strategy, you can provide a different implementation of {security-api-url}org/springframework/security/web/server/authentication/session/ServerMaximumSessionsExceededHandler.html[ServerMaximumSessionsExceededHandler]. +By default, when the maximum number of sessions is exceeded, the least recently used session(s) will be expired by using the javadoc:org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler[]. +Spring Security also provides another implementation that prevents the user from creating new sessions by using the javadoc:org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler[]. +If you want to use your own strategy, you can provide a different implementation of javadoc:org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler[]. .Configuring maximumSessionsExceededHandler [tabs] @@ -254,9 +254,9 @@ open fun reactiveSessionRegistry(): ReactiveSessionRegistry { [[reactive-concurrent-sessions-control-specify-session-registry]] == Specifying a `ReactiveSessionRegistry` -In order to keep track of the user's sessions, Spring Security uses a {security-api-url}org/springframework/security/core/session/ReactiveSessionRegistry.html[ReactiveSessionRegistry], and, every time a user logs in, their session information is saved. +In order to keep track of the user's sessions, Spring Security uses a javadoc:org.springframework.security.core.session.ReactiveSessionRegistry[], and, every time a user logs in, their session information is saved. -Spring Security ships with {security-api-url}org/springframework/security/core/session/InMemoryReactiveSessionRegistry.html[InMemoryReactiveSessionRegistry] implementation of `ReactiveSessionRegistry`. +Spring Security ships with javadoc:org.springframework.security.core.session.InMemoryReactiveSessionRegistry[] implementation of `ReactiveSessionRegistry`. To specify a `ReactiveSessionRegistry` implementation you can either declare it as a bean: diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/bearer-tokens.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/bearer-tokens.adoc index 33362f61a2c..47905d6eb03 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/bearer-tokens.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/bearer-tokens.adoc @@ -41,7 +41,7 @@ return http { == Bearer Token Propagation Now that you have a bearer token, you can pass that to downstream services. -This is possible with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.html[ServerBearerExchangeFilterFunction]`: +This is possible with javadoc:org.springframework.security.oauth2.server.resource.web.reactive.function.client.ServerBearerExchangeFilterFunction[]: [tabs] ====== @@ -70,7 +70,7 @@ fun rest(): WebClient { ---- ====== -When the `WebClient` shown in the preceding example performs requests, Spring Security looks up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential. +When the `WebClient` shown in the preceding example performs requests, Spring Security looks up the current `Authentication` and extract any javadoc:org.springframework.security.oauth2.core.AbstractOAuth2Token[] credential. Then, it propagates that token in the `Authorization` header -- for example: [tabs] diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc index 3b291ed4bcb..b3d14fdb393 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc @@ -234,7 +234,7 @@ fun jwtDecoder(): ReactiveJwtDecoder { [NOTE] ==== -Calling `{security-api-url}org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation]` invokes the Provider Configuration or Authorization Server Metadata endpoint to derive the JWK Set URI. +Calling javadoc:org.springframework.security.oauth2.jwt.ReactiveJwtDecoders#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation] invokes the Provider Configuration or Authorization Server Metadata endpoint to derive the JWK Set URI. If the application does not expose a `ReactiveJwtDecoder` bean, Spring Boot exposes the above default one. ==== 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 8cab012346d..ccdcbf5e9a6 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 @@ -68,7 +68,7 @@ Given an Opaque Token, Resource Server: . Inspects the response for an `{ 'active' : true }` attribute. . Maps each scope to an authority with a prefix of `SCOPE_`. -By default, the resulting `Authentication#getPrincipal` is a Spring Security `{security-api-url}org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html[OAuth2AuthenticatedPrincipal]` object, and `Authentication#getName` maps to the token's `sub` property, if one is present. +By default, the resulting `Authentication#getPrincipal` is a Spring Security javadoc:org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal[] object, and `Authentication#getName` maps to the token's `sub` property, if one is present. From here, you may want to jump to: diff --git a/docs/modules/ROOT/pages/reactive/test/web/oauth2.adoc b/docs/modules/ROOT/pages/reactive/test/web/oauth2.adoc index c43838c5469..2548d96cf50 100644 --- a/docs/modules/ROOT/pages/reactive/test/web/oauth2.adoc +++ b/docs/modules/ROOT/pages/reactive/test/web/oauth2.adoc @@ -956,7 +956,7 @@ client ---- ====== -You can also specify a complete `Jwt`, for which `{security-api-url}org/springframework/security/oauth2/jwt/Jwt.Builder.html[Jwt.Builder]` is quite handy: +You can also specify a complete `Jwt`, for which javadoc:org.springframework.security.oauth2.jwt.Jwt$Builder[] is quite handy: [tabs] ====== diff --git a/docs/modules/ROOT/pages/servlet/architecture.adoc b/docs/modules/ROOT/pages/servlet/architecture.adoc index 01139c110d9..657cbc552c1 100644 --- a/docs/modules/ROOT/pages/servlet/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/architecture.adoc @@ -118,7 +118,7 @@ image::{figures}/filterchainproxy.png[] [[servlet-securityfilterchain]] == SecurityFilterChain -{security-api-url}org/springframework/security/web/SecurityFilterChain.html[`SecurityFilterChain`] is used by <> to determine which Spring Security `Filter` instances should be invoked for the current request. +javadoc:org.springframework.security.web.SecurityFilterChain[] is used by <> to determine which Spring Security `Filter` instances should be invoked for the current request. The following image shows the role of `SecurityFilterChain`. @@ -250,11 +250,11 @@ If you want to see the list of filters invoked for a particular request, you can Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request. For example, you want to make sure that the <> is in the list of the security filters. -The list of filters is printed at INFO level on the application startup, so you can see something like the following on the console output for example: +The list of filters is printed at DEBUG level on the application startup, so you can see something like the following on the console output for example: [source,text,role="terminal"] ---- -2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ +2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ org.springframework.security.web.session.DisableEncodeUrlFilter@404db674, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5, org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7, @@ -392,7 +392,7 @@ public FilterRegistrationBean tenantFilterRegistration(TenantFilte == Handling Security Exceptions -The {security-api-url}org/springframework/security/web/access/ExceptionTranslationFilter.html[`ExceptionTranslationFilter`] allows translation of {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`] and {security-api-url}/org/springframework/security/core/AuthenticationException.html[`AuthenticationException`] into HTTP responses. +The javadoc:org.springframework.security.web.access.ExceptionTranslationFilter[] allows translation of javadoc:org.springframework.security.access.AccessDeniedException[] and javadoc:org.springframework.security.core.AuthenticationException[] into HTTP responses. `ExceptionTranslationFilter` is inserted into the <> as one of the <>. @@ -447,7 +447,7 @@ In Spring Security this is done by saving the `HttpServletRequest` using a <> uses the `RequestCache` to get the saved `HttpServletRequest` after the user authenticates, while the `ExceptionTranslationFilter` uses the `RequestCache` to save the `HttpServletRequest` after it detects `AuthenticationException`, before redirecting the user to the login endpoint. @@ -463,7 +463,7 @@ There are a number of reasons you may want to not store the user's unauthenticat You may want to offload that storage onto the user's browser or store it in a database. Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login. -To do that, you can use {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[the `NullRequestCache` implementation]. +To do that, you can use the javadoc:org.springframework.security.web.savedrequest.NullRequestCache[NullRequestCache] implementation. .Prevent the Request From Being Saved [tabs] @@ -517,7 +517,7 @@ XML:: [[requestcacheawarefilter]] === RequestCacheAwareFilter -The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <> to replay the original request. +The javadoc:org.springframework.security.web.savedrequest.RequestCacheAwareFilter[] uses the <> to replay the original request. [[servlet-logging]] == Logging diff --git a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc index 725fd46d7ec..4757b78ea8d 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc @@ -117,13 +117,13 @@ However, if you do, take a look at the JavaDoc for `SecurityContextHolder` to le [[servlet-authentication-securitycontext]] == SecurityContext -The {security-api-url}org/springframework/security/core/context/SecurityContext.html[`SecurityContext`] is obtained from the <>. +The javadoc:org.springframework.security.core.context.SecurityContext[] is obtained from the <>. The `SecurityContext` contains an <> object. [[servlet-authentication-authentication]] == Authentication -The {security-api-url}org/springframework/security/core/Authentication.html[`Authentication`] interface serves two main purposes within Spring Security: +The javadoc:org.springframework.security.core.Authentication[] interface serves two main purposes within Spring Security: * An input to <> to provide the credentials a user has provided to authenticate. When used in this scenario, `isAuthenticated()` returns `false`. @@ -141,7 +141,7 @@ Two examples are roles and scopes. [[servlet-authentication-granted-authority]] == GrantedAuthority -{security-api-url}org/springframework/security/core/GrantedAuthority.html[`GrantedAuthority`] instances are high-level permissions that the user is granted. +javadoc:org.springframework.security.core.GrantedAuthority[] instances are high-level permissions that the user is granted. Two examples are roles and scopes. You can obtain `GrantedAuthority` instances from the <> method. @@ -160,7 +160,7 @@ Of course, Spring Security is expressly designed to handle this common requireme [[servlet-authentication-authenticationmanager]] == AuthenticationManager -{security-api-url}org/springframework/security/authentication/AuthenticationManager.html[`AuthenticationManager`] is the API that defines how Spring Security's Filters perform xref:features/authentication/index.adoc#authentication[authentication]. +javadoc:org.springframework.security.authentication.AuthenticationManager[] is the API that defines how Spring Security's Filters perform xref:features/authentication/index.adoc#authentication[authentication]. The <> that is returned is then set on the <> by the controller (that is, by xref:servlet/architecture.adoc#servlet-security-filters[Spring Security's `Filters` instances]) that invoked the `AuthenticationManager`. If you are not integrating with Spring Security's `Filters` instances, you can set the `SecurityContextHolder` directly and are not required to use an `AuthenticationManager`. @@ -170,7 +170,7 @@ While the implementation of `AuthenticationManager` could be anything, the most [[servlet-authentication-providermanager]] == ProviderManager -{security-api-url}org/springframework/security/authentication/ProviderManager.html[`ProviderManager`] is the most commonly used implementation of <>. +javadoc:org.springframework.security.authentication.ProviderManager[] is the most commonly used implementation of <>. `ProviderManager` delegates to a `List` of <> instances. Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide. If none of the configured `AuthenticationProvider` instances can authenticate, authentication fails with a `ProviderNotFoundException`, which is a special `AuthenticationException` that indicates that the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it. @@ -200,19 +200,19 @@ If the `Authentication` contains a reference to an object in the cache (such as You need to take this into account if you use a cache. An obvious solution is to first make a copy of the object, either in the cache implementation or in the `AuthenticationProvider` that creates the returned `Authentication` object. Alternatively, you can disable the `eraseCredentialsAfterAuthentication` property on `ProviderManager`. -See the Javadoc for the {security-api-url}org/springframework/security/authentication/ProviderManager.html[ProviderManager] class. +See the Javadoc for the javadoc:org.springframework.security.authentication.ProviderManager[] class. [[servlet-authentication-authenticationprovider]] == AuthenticationProvider -You can inject multiple {security-api-url}org/springframework/security/authentication/AuthenticationProvider.html[``AuthenticationProvider``s] instances into <>. +You can inject multiple javadoc:org.springframework.security.authentication.AuthenticationProvider[] instances into <>. Each `AuthenticationProvider` performs a specific type of authentication. For example, xref:servlet/authentication/passwords/dao-authentication-provider.adoc#servlet-authentication-daoauthenticationprovider[`DaoAuthenticationProvider`] supports username/password-based authentication, while `JwtAuthenticationProvider` supports authenticating a JWT token. [[servlet-authentication-authenticationentrypoint]] == Request Credentials with `AuthenticationEntryPoint` -{security-api-url}org/springframework/security/web/AuthenticationEntryPoint.html[`AuthenticationEntryPoint`] is used to send an HTTP response that requests credentials from a client. +javadoc:org.springframework.security.web.AuthenticationEntryPoint[] is used to send an HTTP response that requests credentials from a client. Sometimes, a client proactively includes credentials (such as a username and password) to request a resource. In these cases, Spring Security does not need to provide an HTTP response that requests credentials from the client, since they are already included. @@ -229,7 +229,7 @@ The `AuthenticationEntryPoint` implementation might perform a xref:servlet/authe [[servlet-authentication-abstractprocessingfilter]] == AbstractAuthenticationProcessingFilter -{security-api-url}org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html[`AbstractAuthenticationProcessingFilter`] is used as a base `Filter` for authenticating a user's credentials. +javadoc:org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter[] is used as a base `Filter` for authenticating a user's credentials. Before the credentials can be authenticated, Spring Security typically requests the credentials by using <>. Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it. @@ -245,26 +245,26 @@ image:{icondir}/number_2.png[] Next, the <> is cleared out. -* `RememberMeServices.loginFail` is invoked. +* `RememberMeServices.loginFail` is invoked.ƒ If remember me is not configured, this is a no-op. -See the {security-api-url}org/springframework/security/web/authentication/rememberme/package-frame.html[`rememberme`] package. +See the javadoc:org.springframework.security.web.authentication.rememberme.package-summary[rememberme] package. * `AuthenticationFailureHandler` is invoked. -See the {security-api-url}org/springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] interface. +See the javadoc:org.springframework.security.web.authentication.AuthenticationFailureHandler[] interface. image:{icondir}/number_4.png[] If authentication is successful, then __Success__. * `SessionAuthenticationStrategy` is notified of a new login. -See the {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface. +See the javadoc:org.springframework.security.web.authentication.session.SessionAuthenticationStrategy[] interface. * The <> is set on the <>. Later, if you need to save the `SecurityContext` so that it can be automatically set on future requests, `SecurityContextRepository#saveContext` must be explicitly invoked. -See the {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] class. +See the javadoc:org.springframework.security.web.context.SecurityContextHolderFilter[] class. * `RememberMeServices.loginSuccess` is invoked. If remember me is not configured, this is a no-op. -See the {security-api-url}org/springframework/security/web/authentication/rememberme/package-frame.html[`rememberme`] package. +See the javadoc:org.springframework.security.web.authentication.rememberme.package-summary[rememberme] package. * `ApplicationEventPublisher` publishes an `InteractiveAuthenticationSuccessEvent`. * `AuthenticationSuccessHandler` is invoked. -See the {security-api-url}org/springframework/security/web/authentication/AuthenticationSuccessHandler.html[`AuthenticationSuccessHandler`] interface. +See the javadoc:org.springframework.security.web.authentication.AuthenticationSuccessHandler[] interface. // daoauthenticationprovider (goes in username/password) diff --git a/docs/modules/ROOT/pages/servlet/authentication/events.adoc b/docs/modules/ROOT/pages/servlet/authentication/events.adoc index edb8b5146bd..59ad12f0d93 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/events.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/events.adoc @@ -143,10 +143,10 @@ Java:: @Bean public AuthenticationEventPublisher authenticationEventPublisher (ApplicationEventPublisher applicationEventPublisher) { - AuthenticationEventPublisher authenticationEventPublisher = + DefaultAuthenticationEventPublisher authenticationEventPublisher = new DefaultAuthenticationEventPublisher(applicationEventPublisher); authenticationEventPublisher.setDefaultAuthenticationFailureEvent - (GenericAuthenticationFailureEvent.class); + (AbstractAuthenticationFailureEvent.class); return authenticationEventPublisher; } ---- @@ -159,7 +159,7 @@ Kotlin:: fun authenticationEventPublisher (applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher { val authenticationEventPublisher = DefaultAuthenticationEventPublisher(applicationEventPublisher) - authenticationEventPublisher.setDefaultAuthenticationFailureEvent(GenericAuthenticationFailureEvent::class.java) + authenticationEventPublisher.setDefaultAuthenticationFailureEvent(AbstractAuthenticationFailureEvent::class.java) return authenticationEventPublisher } ---- diff --git a/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc b/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc index 44d02db427b..bb2e7ced1b2 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc @@ -58,7 +58,7 @@ This means that `DefaultJaasAuthenticationProvider` is not bound to any particul To make it easy to inject a `Configuration` into `DefaultJaasAuthenticationProvider`, a default in-memory implementation named `InMemoryConfiguration` is provided. The implementation constructor accepts a `Map` where each key represents a login configuration name, and the value represents an `Array` of `AppConfigurationEntry` instances. `InMemoryConfiguration` also supports a default `Array` of `AppConfigurationEntry` objects that is used if no mapping is found within the provided `Map`. -For details, see the {security-api-url}org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.html[Javadoc of `InMemoryConfiguration`]. +For details, see the Javadoc of javadoc:org.springframework.security.authentication.jaas.memory.InMemoryConfiguration[]. [[jaas-djap-config]] diff --git a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc index 91d669ce938..45289c2ed53 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc @@ -30,16 +30,16 @@ Please note that if xref:servlet/exploits/csrf.adoc[CSRF protection] is disabled In your application it is not necessary to use `GET /logout` to perform a logout. So long as xref:servlet/exploits/csrf.adoc[the needed CSRF token] is present in the request, your application can simply `POST /logout` to induce a logout. -If you request `POST /logout`, then it will perform the following default operations using a series of {security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[``LogoutHandler``]s: +If you request `POST /logout`, then it will perform the following default operations using a series of javadoc:org.springframework.security.web.authentication.logout.LogoutHandler[] instances: -- Invalidate the HTTP session ({security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`]) -- Clear the xref:servlet/authentication/session-management.adoc#use-securitycontextholderstrategy[`SecurityContextHolderStrategy`] ({security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`]) -- Clear the xref:servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`] ({security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`]) +- Invalidate the HTTP session (javadoc:org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler[]) +- Clear the xref:servlet/authentication/session-management.adoc#use-securitycontextholderstrategy[`SecurityContextHolderStrategy`] (javadoc:org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler[]) +- Clear the xref:servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`] (javadoc:org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler[]) - Clean up any xref:servlet/authentication/rememberme.adoc[RememberMe authentication] (`TokenRememberMeServices` / `PersistentTokenRememberMeServices`) -- Clear out any saved xref:servlet/exploits/csrf.adoc[CSRF token] ({security-api-url}org/springframework/security/web/csrf/CsrfLogoutHandler.html[`CsrfLogoutHandler`]) -- xref:servlet/authentication/events.adoc[Fire] a `LogoutSuccessEvent` ({security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessEventPublishingLogoutHandler.html[`LogoutSuccessEventPublishingLogoutHandler`]) +- Clear out any saved xref:servlet/exploits/csrf.adoc[CSRF token] (javadoc:org.springframework.security.web.csrf.CsrfLogoutHandler[]) +- xref:servlet/authentication/events.adoc[Fire] a `LogoutSuccessEvent` (javadoc:org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler[]) -Once completed, then it will exercise its default {security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[`LogoutSuccessHandler`] which redirects to `/login?logout`. +Once completed, then it will exercise its default javadoc:org.springframework.security.web.authentication.logout.LogoutSuccessHandler[] which redirects to `/login?logout`. [[customizing-logout-uris]] == Customizing Logout URIs @@ -197,15 +197,15 @@ http { ====== [NOTE] -Because {security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[``LogoutHandler``]s are for the purposes of cleanup, they should not throw exceptions. +Because javadoc:org.springframework.security.web.authentication.logout.LogoutHandler[] instances are for the purposes of cleanup, they should not throw exceptions. [TIP] -Since {security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[`LogoutHandler`] is a functional interface, you can provide a custom one as a lambda. +Since javadoc:org.springframework.security.web.authentication.logout.LogoutHandler[] is a functional interface, you can provide a custom one as a lambda. Some logout handler configurations are common enough that they are exposed directly in the `logout` DSL and `` element. One example is configuring session invalidation and another is which additional cookies should be deleted. -For example, you can configure the {security-api-url}org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.html[`CookieClearingLogoutHandler`] as seen above. +For example, you can configure the javadoc:org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler[] as seen above. [[delete-cookies]] Or you can instead set the appropriate configuration value like so: @@ -242,7 +242,7 @@ Xml:: ====== [NOTE] -Specifying that the `JSESSIONID` cookie is not necessary since {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`] removes it by virtue of invalidating the session. +Specifying that the `JSESSIONID` cookie is not necessary since javadoc:org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler[] removes it by virtue of invalidating the session. [[clear-all-site-data]] === Using Clear-Site-Data to Log Out the User @@ -310,7 +310,7 @@ http { == Customizing Logout Success While using `logoutSuccessUrl` will suffice for most cases, you may need to do something different from redirecting to a URL once logout is complete. -{security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[`LogoutSuccessHandler`] is the Spring Security component for customizing logout success actions. +javadoc:org.springframework.security.web.authentication.logout.LogoutSuccessHandler[] is the Spring Security component for customizing logout success actions. For example, instead of redirecting, you may want to only return a status code. In this case, you can provide a success handler instance, like so: @@ -349,7 +349,7 @@ Xml:: ====== [TIP] -Since {security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[`LogoutSuccessHandler`] is a functional interface, you can provide a custom one as a lambda. +Since javadoc:org.springframework.security.web.authentication.logout.LogoutSuccessHandler[] is a functional interface, you can provide a custom one as a lambda. [[creating-custom-logout-endpoint]] == Creating a Custom Logout Endpoint @@ -387,7 +387,7 @@ fun performLogout(): String { ---- ====== -then you will need to have that endpoint invoke Spring Security's {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`] to ensure a secure and complete logout. +then you will need to have that endpoint invoke Spring Security's javadoc:org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler[] to ensure a secure and complete logout. Something like the following is needed at a minimum: .Custom Logout Endpoint @@ -422,12 +422,12 @@ fun performLogout(val authentication: Authentication, val request: HttpServletRe ---- ====== -Such will clear out the {security-api-url}/org/springframework/security/core/context/SecurityContextHolderStrategy.html[`SecurityContextHolderStrategy`] and {security-api-url}/org/springframework/security/web/context/SecurityContextRepository.html[`SecurityContextRepository`] as needed. +Such will clear out the javadoc:org.springframework.security.core.context.SecurityContextHolderStrategy[] and javadoc:org.springframework.security.web.context.SecurityContextRepository[] as needed. Also, you'll need to <>. [WARNING] -Failing to call {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`] means that xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[the `SecurityContext`] could still be available on subsequent requests, meaning that the user is not actually logged out. +Failing to call javadoc:org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler[] means that xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[the `SecurityContext`] could still be available on subsequent requests, meaning that the user is not actually logged out. [[testing-logout]] == Testing Logout diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc index aeecd5e60b6..a3d2a6a9f84 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc @@ -18,7 +18,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. -The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/www/BasicAuthenticationEntryPoint.html[`BasicAuthenticationEntryPoint`], which sends a WWW-Authenticate header. +The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of javadoc:org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint[], which sends a WWW-Authenticate header. The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested. When a client receives the `WWW-Authenticate` header, it knows it should retry with a username and password. @@ -41,18 +41,18 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__. . The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out. . `RememberMeServices.loginFail` is invoked. If remember me is not configured, this is a no-op. -See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc. +See the javadoc:org.springframework.security.web.authentication.RememberMeServices[] interface in the Javadoc. . `AuthenticationEntryPoint` is invoked to trigger the WWW-Authenticate to be sent again. -See the {security-api-url}org/springframework/security/web/AuthenticationEntryPoint.html[`AuthenticationEntryPoint`] interface in the Javadoc. +See the javadoc:org.springframework.security.web.AuthenticationEntryPoint[] interface in the Javadoc. image:{icondir}/number_4.png[] If authentication is successful, then __Success__. . The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. . `RememberMeServices.loginSuccess` is invoked. If remember me is not configured, this is a no-op. -See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc. +See the javadoc:org.springframework.security.web.authentication.RememberMeServices[] interface in the Javadoc. . The `BasicAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic. -See the {security-api-url}org/springframework/security/web/authentication/www/BasicAuthenticationFilter.html[`BasicAuthenticationFilter`] Class in the Javadoc +See the javadoc:org.springframework.security.web.authentication.www.BasicAuthenticationFilter[] Class in the Javadoc By default, Spring Security's HTTP Basic Authentication support is enabled. However, as soon as any servlet based configuration is provided, HTTP Basic must be explicitly provided. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/credentials-container.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/credentials-container.adoc new file mode 100644 index 00000000000..3f8d22da0ac --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/credentials-container.adoc @@ -0,0 +1,12 @@ +[[servlet-authentication-credentialscontainer]] += CredentialsContainer + +The javadoc:org.springframework.security.core.CredentialsContainer[] interface indicates that the implementing object contains sensitive data, and is used internally by Spring Security to erase the authentication credentials after a successful authentication. +This interface is implemented by most of Spring Security internal domain classes, like javadoc:org.springframework.security.core.userdetails.User[] and javadoc:org.springframework.security.authentication.UsernamePasswordAuthenticationToken[]. + +The `ProviderManager` manager checks whether the returned `Authentication` implements this interface. +If so, xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager-erasing-credentials[it calls the `eraseCredentials` method] to remove the credentials from the object. + +If you want your custom authentication objects to have their credentials erased after authentication, you should ensure that the classes implement the `CredentialsContainer` interface. + +Users who are writing their own `AuthenticationProvider` implementations should create and return an appropriate `Authentication` object there, minus any sensitive data, rather than using this interface. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/dao-authentication-provider.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/dao-authentication-provider.adoc index 1366308c7c6..ba27f541a2e 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/dao-authentication-provider.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/dao-authentication-provider.adoc @@ -2,7 +2,7 @@ = DaoAuthenticationProvider :figures: servlet/authentication/unpwd -{security-api-url}org/springframework/security/authentication/dao/DaoAuthenticationProvider.html[`DaoAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that uses a xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] and xref:servlet/authentication/passwords/password-encoder.adoc#servlet-authentication-password-storage[`PasswordEncoder`] to authenticate a username and password. +javadoc:org.springframework.security.authentication.dao.DaoAuthenticationProvider[] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that uses a xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] and xref:servlet/authentication/passwords/password-encoder.adoc#servlet-authentication-password-storage[`PasswordEncoder`] to authenticate a username and password. This section examines how `DaoAuthenticationProvider` works within Spring Security. The following figure explains the workings of the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc index 962a7523da0..e4af5ebeaf8 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc @@ -33,7 +33,7 @@ key: A private key to prevent modification of the nonce token ---- You need to ensure that you xref:features/authentication/password-storage.adoc#authentication-password-storage-configuration[configure] insecure plain text xref:features/authentication/password-storage.adoc#authentication-password-storage[Password Storage] using `NoOpPasswordEncoder`. -(See the {security-api-url}org/springframework/security/crypto/password/NoOpPasswordEncoder.html[`NoOpPasswordEncoder`] class in the Javadoc.) +(See the javadoc:org.springframework.security.crypto.password.NoOpPasswordEncoder[] class in the Javadoc.) The following provides an example of configuring Digest Authentication with Java Configuration: .Digest Authentication diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc index 91f211e3e64..207ddb76549 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc @@ -19,7 +19,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__ and sends a redirect to the login page with the configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`]. -In most cases, the `AuthenticationEntryPoint` is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`]. +In most cases, the `AuthenticationEntryPoint` is an instance of javadoc:org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint[]. image:{icondir}/number_4.png[] The browser requests the login page to which it was redirected. @@ -45,19 +45,19 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__. . The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out. . `RememberMeServices.loginFail` is invoked. If remember me is not configured, this is a no-op. -See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc. +See the javadoc:org.springframework.security.web.authentication.RememberMeServices[] interface in the Javadoc. . `AuthenticationFailureHandler` is invoked. -See the {security-api-url}org/springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] class in the Javadoc +See the javadoc:org.springframework.security.web.authentication.AuthenticationFailureHandler[] class in the Javadoc image:{icondir}/number_4.png[] If authentication is successful, then __Success__. . `SessionAuthenticationStrategy` is notified of a new login. -See the {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface in the Javadoc. +See the javadoc:org.springframework.security.web.authentication.session.SessionAuthenticationStrategy[] interface in the Javadoc. . The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. -See the {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class in the Javadoc. +See the javadoc:org.springframework.security.web.context.SecurityContextPersistenceFilter[] class in the Javadoc. . `RememberMeServices.loginSuccess` is invoked. If remember me is not configured, this is a no-op. -See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc. +See the javadoc:org.springframework.security.web.authentication.RememberMeServices[] interface in the Javadoc. . `ApplicationEventPublisher` publishes an `InteractiveAuthenticationSuccessEvent`. . The `AuthenticationSuccessHandler` is invoked. Typically, this is a `SimpleUrlAuthenticationSuccessHandler`, which redirects to a request saved by xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] when we redirect to the login page. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details-service.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details-service.adoc index af6ed15daa5..fcfd20c7ece 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details-service.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details-service.adoc @@ -1,7 +1,7 @@ [[servlet-authentication-userdetailsservice]] = UserDetailsService -{security-api-url}org/springframework/security/core/userdetails/UserDetailsService.html[`UserDetailsService`] is used by xref:servlet/authentication/passwords/dao-authentication-provider.adoc#servlet-authentication-daoauthenticationprovider[`DaoAuthenticationProvider`] for retrieving a username, a password, and other attributes for authenticating with a username and password. +javadoc:org.springframework.security.core.userdetails.UserDetailsService[] is used by xref:servlet/authentication/passwords/dao-authentication-provider.adoc#servlet-authentication-daoauthenticationprovider[`DaoAuthenticationProvider`] for retrieving a username, a password, and other attributes for authenticating with a username and password. Spring Security provides xref:servlet/authentication/passwords/in-memory.adoc#servlet-authentication-inmemory[in-memory], xref:servlet/authentication/passwords/jdbc.adoc#servlet-authentication-jdbc[JDBC], and xref:servlet/authentication/passwords/caching.adoc#servlet-authentication-caching-user-details[caching] implementations of `UserDetailsService`. You can define custom authentication by exposing a custom `UserDetailsService` as a bean. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details.adoc index 34918cb455a..292f119018a 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/user-details.adoc @@ -1,5 +1,5 @@ [[servlet-authentication-userdetails]] = UserDetails -{security-api-url}org/springframework/security/core/userdetails/UserDetails.html[`UserDetails`] is returned by the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`]. +javadoc:org.springframework.security.core.userdetails.UserDetails[] is returned by the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`]. The xref:servlet/authentication/passwords/dao-authentication-provider.adoc#servlet-authentication-daoauthenticationprovider[`DaoAuthenticationProvider`] validates the `UserDetails` and then returns an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that has a principal that is the `UserDetails` returned by the configured `UserDetailsService`. diff --git a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc index 1528d66787b..0a43f504664 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc @@ -59,8 +59,8 @@ Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8 == 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`]. -The default implementation of `SecurityContextRepository` is {security-api-url}org/springframework/security/web/context/DelegatingSecurityContextRepository.html[`DelegatingSecurityContextRepository`] which delegates to the following: +In Spring Security the association of the user to future requests is made using javadoc:org.springframework.security.web.context.SecurityContextRepository[]. +The default implementation of `SecurityContextRepository` is javadoc:org.springframework.security.web.context.DelegatingSecurityContextRepository[] which delegates to the following: * <> * <> @@ -68,18 +68,18 @@ The default implementation of `SecurityContextRepository` is {security-api-url}o [[httpsecuritycontextrepository]] === HttpSessionSecurityContextRepository -The {security-api-url}org/springframework/security/web/context/HttpSessionSecurityContextRepository.html[`HttpSessionSecurityContextRepository`] associates the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[`SecurityContext`] to the `HttpSession`. +The javadoc:org.springframework.security.web.context.HttpSessionSecurityContextRepository[] 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. +If it is not desirable to associate the `SecurityContext` to an `HttpSession` (i.e. when authenticating with OAuth) the javadoc:org.springframework.security.web.context.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 available for a single request that occurs across dispatch types that may clear out the `SecurityContext`. +The javadoc:org.springframework.security.web.context.RequestAttributeSecurityContextRepository[] saves the `SecurityContext` as a request attribute to make sure the `SecurityContext` is available 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. @@ -118,7 +118,7 @@ XML:: [[delegatingsecuritycontextrepository]] === DelegatingSecurityContextRepository -The {security-api-url}org/springframework/security/web/context/DelegatingSecurityContextRepository.html[`DelegatingSecurityContextRepository`] saves the `SecurityContext` to multiple `SecurityContextRepository` delegates and allows retrieval from any of the delegates in a specified order. +The javadoc:org.springframework.security.web.context.DelegatingSecurityContextRepository[] saves the `SecurityContext` to multiple `SecurityContextRepository` delegates and allows retrieval from any of the delegates in a specified order. The most useful arrangement for this is configured with the following example, which allows the use of both xref:requestattributesecuritycontextrepository[`RequestAttributeSecurityContextRepository`] and xref:httpsecuritycontextrepository[`HttpSessionSecurityContextRepository`] simultaneously. @@ -189,7 +189,7 @@ In Spring Security 6, the example shown above is the default configuration. [[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`]. +The javadoc:org.springframework.security.web.context.SecurityContextPersistenceFilter[] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`]. image::{figures}/securitycontextpersistencefilter.png[] @@ -210,7 +210,7 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the ` [[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`]. +The javadoc:org.springframework.security.web.context.SecurityContextHolderFilter[] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`]. image::{figures}/securitycontextholderfilter.png[] diff --git a/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc b/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc index 4b5ec52830d..3e8e24199f7 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc @@ -89,7 +89,7 @@ void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication); ---- -See the Javadoc for {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] for a fuller discussion on what the methods do, although note that, at this stage, `AbstractAuthenticationProcessingFilter` calls only the `loginFail()` and `loginSuccess()` methods. +See the Javadoc for javadoc:org.springframework.security.web.authentication.RememberMeServices[] for a fuller discussion on what the methods do, although note that, at this stage, `AbstractAuthenticationProcessingFilter` calls only the `loginFail()` and `loginSuccess()` methods. The `autoLogin()` method is called by `RememberMeAuthenticationFilter` whenever the `SecurityContextHolder` does not contain an `Authentication`. This interface, therefore, provides the underlying remember-me implementation with sufficient notification of authentication-related events and delegates to the implementation whenever a candidate web request might contain a cookie and wish to be remembered. This design allows any number of remember-me implementation strategies. diff --git a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc index f269c960bc6..fe0821a31df 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc @@ -49,7 +49,7 @@ The latter is also used when configuring an invalid session URL through the name [[moving-away-from-sessionmanagementfilter]] ==== Moving Away From `SessionManagementFilter` -In Spring Security 5, the default configuration relies on `SessionManagementFilter` to detect if a user just authenticated and invoke {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[the `SessionAuthenticationStrategy`]. +In Spring Security 5, the default configuration relies on `SessionManagementFilter` to detect if a user just authenticated and invoke the javadoc:org.springframework.security.web.authentication.session.SessionAuthenticationStrategy[SessionAuthenticationStrategy]. The problem with this is that it means that in a typical setup, the `HttpSession` must be read for every request. In Spring Security 6, the default is that authentication mechanisms themselves must invoke the `SessionAuthenticationStrategy`. @@ -63,10 +63,10 @@ In Spring Security 6, the `SessionManagementFilter` is not used by default, ther |Method |Replacement |`sessionAuthenticationErrorUrl` -|Configure an {security-api-url}/org/springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] in your authentication mechanism +|Configure an javadoc:org.springframework.security.web.authentication.AuthenticationFailureHandler[] in your authentication mechanism |`sessionAuthenticationFailureHandler` -|Configure an {security-api-url}/org/springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] in your authentication mechanism +|Configure an javadoc:org.springframework.security.web.authentication.AuthenticationFailureHandler[] in your authentication mechanism |`sessionAuthenticationStrategy` |Configure an `SessionAuthenticationStrategy` in your authentication mechanism as <> @@ -589,8 +589,8 @@ If that is your case, you might want to < Allow access to URLs that start with `/admin/` to users with the `ADMIN` role <4> Any other request that doesn't match the rules above, will require authentication -The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If {spring-framework-reference-url}web.html#spring-web[Spring MVC] is in the classpath, then {security-api-url}org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.html[`MvcRequestMatcher`] will be used, otherwise, {security-api-url}org/springframework/security/web/servlet/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] will be used. +The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If {spring-framework-reference-url}web.html#spring-web[Spring MVC] is in the classpath, then javadoc:org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher[] will be used, otherwise, javadoc:org.springframework.security.web.util.matcher.AntPathRequestMatcher[] will be used. You can read more about the Spring MVC integration xref:servlet/integrations/mvc.adoc[here]. If you want to use a specific `RequestMatcher`, just pass an implementation to the `securityMatcher` and/or `requestMatcher` methods: diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index d6c6e0320fc..ff74f7ef2ef 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -117,15 +117,15 @@ A given invocation to `MyCustomerService#readCustomer` may look something like t image::{figures}/methodsecurity.png[] -1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <> -2. The interceptor invokes {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager#check`] +1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[] that matches <> +2. The interceptor invokes javadoc:org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager[`PreAuthorizeAuthorizationManager#check`] 3. The authorization manager uses a `MethodSecurityExpressionHandler` to parse the annotation's <> and constructs a corresponding `EvaluationContext` from a `MethodSecurityExpressionRoot` containing xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[a `Supplier`] and `MethodInvocation`. 4. The interceptor uses this context to evaluate the expression; specifically, it reads xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] from the `Supplier` and checks whether it has `permission:read` in its collection of xref:servlet/authorization/architecture.adoc#authz-authorities[authorities] 5. If the evaluation passes, then Spring AOP proceeds to invoke the method. -6. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`] which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response -7. After the method returns, Spring AOP invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[`AuthorizationManagerAfterMethodInterceptor`] that matches <>, operating the same as above, but with {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[`PostAuthorizeAuthorizationManager`] +6. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an javadoc:org.springframework.security.access.AccessDeniedException[] which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response +7. After the method returns, Spring AOP invokes an javadoc:org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor[] that matches <>, operating the same as above, but with javadoc:org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager[] 8. If the evaluation passes (in this case, the return value belongs to the logged-in user), processing continues normally -9. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`], which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response +9. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an javadoc:org.springframework.security.access.AccessDeniedException[], which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response [NOTE] If the method is not being called in the context of an HTTP request, you will likely need to handle the `AccessDeniedException` yourself @@ -150,7 +150,7 @@ Instead, use SpEL's boolean support or its support for delegating to a separate Each annotation has its own pointcut instance that looks for that annotation or its <> counterparts across the entire object hierarchy, starting at <>. -You can see the specifics of this in {security-api-url}org/springframework/security/authorization/method/AuthorizationMethodPointcuts.html[`AuthorizationMethodPointcuts`]. +// FIXME: AuthorizationMethodPointcuts is package private and Javadoc is not published You can see the specifics of this in javadoc:org.springframework.security.authorization.method.AuthorizationMethodPointcuts[]. [[annotation-method-interceptors]] === Each Annotation Has Its Own Method Interceptor @@ -161,12 +161,12 @@ For example, if needed, you can disable the Spring Security defaults and <<_enab The method interceptors are as follows: -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor#preAuthorize`], which in turn uses {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[`AuthorizationManagerAfterMethodInterceptor#postAuthorize`], which in turn uses {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[`PostAuthorizeAuthorizationManager`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.html[`PreFilterAuthorizationMethodInterceptor`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.html[`PostFilterAuthorizationMethodInterceptor`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor#secured`], which in turn uses {security-api-url}org/springframework/security/authorization/method/SecuredAuthorizationManager.html[`SecuredAuthorizationManager`] -* For JSR-250 annotations, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor#jsr250`], which in turn uses {security-api-url}org/springframework/security/authorization/method/Jsr250AuthorizationManager.html[`Jsr250AuthorizationManager`] +* For <>, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[`AuthorizationManagerBeforeMethodInterceptor#preAuthorize`], which in turn uses javadoc:org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager[] +* For <>, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor[`AuthorizationManagerAfterMethodInterceptor#postAuthorize`], which in turn uses javadoc:org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager[] +* For <>, Spring Security uses javadoc:org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor[] +* For <>, Spring Security uses javadoc:org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor[] +* For <>, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[`AuthorizationManagerBeforeMethodInterceptor#secured`], which in turn uses javadoc:org.springframework.security.authorization.method.SecuredAuthorizationManager[] +* For JSR-250 annotations, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[`AuthorizationManagerBeforeMethodInterceptor#jsr250`], which in turn uses javadoc:org.springframework.security.authorization.method.Jsr250AuthorizationManager[] Generally speaking, you can consider the following listing as representative of what interceptors Spring Security publishes when you add `@EnableMethodSecurity`: @@ -240,7 +240,7 @@ static RoleHierarchy roleHierarchy() { Kotlin:: + -[source,java,role="secondary"] +[source,kotlin,role="secondary"] ---- companion object { @Bean @@ -311,7 +311,7 @@ The primary way Spring Security enables method-level authorization support is th [[use-preauthorize]] === Authorizing Method Invocation with `@PreAuthorize` -When <>, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreAuthorize.html[`@PreAuthorize`] annotation like so: +When <>, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PreAuthorize[format=annotation] annotation like so: [tabs] ====== @@ -399,7 +399,7 @@ While `@PreAuthorize` is quite helpful for declaring needed authorities, it can [[use-postauthorize]] === Authorization Method Results with `@PostAuthorize` -When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostAuthorize.html[`@PostAuthorize`] annotation like so: +When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PostAuthorize[format=annotation] annotation like so: [tabs] ====== @@ -546,7 +546,7 @@ If not, Spring Security will throw an `AccessDeniedException` and return a 403 s [[use-prefilter]] === Filtering Method Parameters with `@PreFilter` -When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreFilter.html[`@PreFilter`] annotation like so: +When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PreFilter[format=annotation] annotation like so: [tabs] ====== @@ -670,7 +670,7 @@ The result is that the above method will only have the `Account` instances where [[use-postfilter]] === Filtering Method Results with `@PostFilter` -When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostFilter.html[`@PostFilter`] annotation like so: +When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PostFilter[format=annotation] annotation like so: [tabs] ====== @@ -795,7 +795,7 @@ In-memory filtering can obviously be expensive, and so be considerate of whether [[use-secured]] === Authorizing Method Invocation with `@Secured` -{security-api-url}org/springframework/security/access/annotation/Secured.html[`@Secured`] is a legacy option for authorizing invocations. +javadoc:org.springframework.security.access.annotation.Secured[format=annotation] is a legacy option for authorizing invocations. <> supercedes it and is recommended instead. To use the `@Secured` annotation, you should first change your Method Security declaration to enable it like so: @@ -1475,7 +1475,7 @@ You can place your interceptor in between Spring Security method interceptors us === Customizing Expression Handling Or, third, you can customize how each SpEL expression is handled. -To do that, you can expose a custom {security-api-url}org.springframework.security.access.expression.method.MethodSecurityExpressionHandler.html[`MethodSecurityExpressionHandler`], like so: +To do that, you can expose a custom javadoc:org.springframework.security.access.expression.method.MethodSecurityExpressionHandler[], like so: .Custom MethodSecurityExpressionHandler [tabs] @@ -1804,7 +1804,7 @@ The intention of this expression is to require that the current `Authentication` + Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. -* If xref:servlet/integrations/data.adoc[Spring Data's] `@Param` annotation is present on at least one parameter for the method, the value is used. +2. If xref:servlet/integrations/data.adoc[Spring Data's] `@Param` annotation is present on at least one parameter for the method, the value is used. The following example uses the `@Param` annotation: + [tabs] @@ -1838,10 +1838,10 @@ The intention of this expression is to require that `name` be equal to `Authenti + Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. -* If you compile your code with the `-parameters` argument, the standard JDK reflection API is used to discover the parameter names. +3. If you compile your code with the `-parameters` argument, the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces. -* Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols. +4. Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols. This does not work for interfaces, since they do not have debug information about the parameter names. For interfaces, either annotations or the `-parameters` approach must be used. @@ -2357,8 +2357,8 @@ You can also add the Spring Boot property `spring.jackson.default-property-inclu There are some scenarios where you may not wish to throw an `AuthorizationDeniedException` when a method is invoked without the required permissions. Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where authorization denied happened before invoking the method. -Spring Security provides support for handling authorization denied on method invocation by using the {security-api-url}org/springframework/security/authorization/method/HandleAuthorizationDenied.html[`@HandleAuthorizationDenied`]. -The handler works for denied authorizations that happened in the <> as well as {security-api-url}org/springframework/security/authorization/AuthorizationDeniedException.html[`AuthorizationDeniedException`] thrown from the method invocation itself. +Spring Security provides support for handling authorization denied on method invocation by using the javadoc:org.springframework.security.authorization.method.HandleAuthorizationDenied[format=annotation]. +The handler works for denied authorizations that happened in the <> as well as javadoc:org.springframework.security.authorization.AuthorizationDeniedException[] thrown from the method invocation itself. Let's consider the example from the <>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@HandleAuthorizationDenied`: @@ -2473,7 +2473,7 @@ fun getEmailWhenProxiedThenNullEmail() { There are some scenarios where you might want to return a secure result derived from the denied result. For example, if a user is not authorized to see email addresses, you might want to apply some masking on the original email address, i.e. _useremail@example.com_ would become _use\\******@example.com_. -For those scenarios, you can override the `handleDeniedInvocationResult` from the `MethodAuthorizationDeniedHandler`, which has the {security-api-url}org/springframework/security/authorization/method/MethodInvocationResult.html[`MethodInvocationResult`] as an argument. +For those scenarios, you can override the `handleDeniedInvocationResult` from the `MethodAuthorizationDeniedHandler`, which has the javadoc:org.springframework.security.authorization.method.MethodInvocationResult[] as an argument. Let's continue with the previous example, but instead of returning `null`, we will return a masked value of the email: [tabs] @@ -2818,7 +2818,7 @@ If you are using `@EnableGlobalMethodSecurity`, you should migrate to `@EnableMe [[servlet-replace-globalmethodsecurity-with-methodsecurity]] === Replace xref:servlet/authorization/method-security.adoc#jc-enable-global-method-security[global method security] with xref:servlet/authorization/method-security.adoc#jc-enable-method-security[method security] -{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[`@EnableGlobalMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[``] are deprecated in favor of {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[`@EnableMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[``], respectively. +javadoc:org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity[format=annotation] and xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[``] are deprecated in favor of javadoc:org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity[`@EnableMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[``], respectively. The new annotation and XML element activate Spring's xref:servlet/authorization/method-security.adoc#jc-enable-method-security[pre-post annotations] by default and use `AuthorizationManager` internally. This means that the following two listings are functionally equivalent: diff --git a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc index 7dd0a4cea0e..68b0906addf 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc @@ -79,20 +79,20 @@ To learn more about CSRF protection for your application, consider the following [[csrf-components]] == Understanding CSRF Protection's Components -CSRF protection is provided by several components that are composed within the {security-api-url}org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`]: +CSRF protection is provided by several components that are composed within the javadoc:org.springframework.security.web.csrf.CsrfFilter[]: .`CsrfFilter` Components image::{figures}/csrf.png[] CSRF protection is divided into two parts: -1. Make the {security-api-url}org/springframework/security/web/csrf/CsrfToken.html[`CsrfToken`] available to the application by delegating to the <>. +1. Make the javadoc:org.springframework.security.web.csrf.CsrfToken[] available to the application by delegating to the <>. 2. Determine if the request requires CSRF protection, load and validate the token, and <>. .`CsrfFilter` Processing image::{figures}/csrf-processing.png[] -* image:{icondir}/number_1.png[] First, the {security-api-url}org/springframework/security/web/csrf/DeferredCsrfToken.html[`DeferredCsrfToken`] is loaded, which holds a reference to the <> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]). +* image:{icondir}/number_1.png[] First, the javadoc:org.springframework.security.web.csrf.DeferredCsrfToken[] is loaded, which holds a reference to the <> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]). * image:{icondir}/number_2.png[] Second, a `Supplier` (created from `DeferredCsrfToken`) is given to the <>, which is responsible for populating a request attribute to make the `CsrfToken` available to the rest of the application. * image:{icondir}/number_3.png[] Next, the main CSRF protection processing begins and checks if the current request requires CSRF protection. If not required, the filter chain is continued and processing ends. * image:{icondir}/number_4.png[] If CSRF protection is required, the persisted `CsrfToken` is finally loaded from the `DeferredCsrfToken`. @@ -128,7 +128,7 @@ You can also specify <> to [[csrf-token-repository-httpsession]] === Using the `HttpSessionCsrfTokenRepository` -By default, Spring Security stores the expected CSRF token in the `HttpSession` by using {security-api-url}org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.html[`HttpSessionCsrfTokenRepository`], so no additional code is necessary. +By default, Spring Security stores the expected CSRF token in the `HttpSession` by using javadoc:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository[], so no additional code is necessary. The `HttpSessionCsrfTokenRepository` reads the token from an HTTP request header named `X-CSRF-TOKEN` or the request parameter `_csrf` by default. @@ -197,7 +197,7 @@ XML:: [[csrf-token-repository-cookie]] === Using the `CookieCsrfTokenRepository` -You can persist the `CsrfToken` in a cookie to <> using the {security-api-url}org/springframework/security/web/csrf/CookieCsrfTokenRepository.html[`CookieCsrfTokenRepository`]. +You can persist the `CsrfToken` in a cookie to <> using the javadoc:org.springframework.security.web.csrf.CookieCsrfTokenRepository[]. The `CookieCsrfTokenRepository` writes to a cookie named `XSRF-TOKEN` and reads it from an HTTP request header named `X-XSRF-TOKEN` or the request parameter `_csrf` by default. These defaults come from Angular and its predecessor https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection[AngularJS]. @@ -280,7 +280,7 @@ If you do not need the ability to read the cookie with JavaScript directly, we _ [[csrf-token-repository-custom]] === Customizing the `CsrfTokenRepository` -There can be cases where you want to implement a custom {security-api-url}org/springframework/security/web/csrf/CsrfTokenRepository.html[`CsrfTokenRepository`]. +There can be cases where you want to implement a custom javadoc:org.springframework.security.web.csrf.CsrfTokenRepository[]. Once you've implemented the `CsrfTokenRepository` interface, you can configure Spring Security to use it with the following configuration: @@ -708,7 +708,7 @@ The following view technologies automatically include the actual CSRF token in a * https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-view-jsp-formtaglib[Spring’s form tag library] * https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor[Thymeleaf] -* Any other view technology that integrates with {spring-framework-api-url}org/springframework/web/servlet/support/RequestDataValueProcessor.html[`RequestDataValueProcessor`] (via {security-api-url}org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.html[`CsrfRequestDataValueProcessor`]) +* Any other view technology that integrates with {spring-framework-api-url}org/springframework/web/servlet/support/RequestDataValueProcessor.html[`RequestDataValueProcessor`] (via javadoc:org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor[]) * You can also include the token yourself via the xref:servlet/integrations/jsp-taglibs.adoc#taglibs-csrfinput[csrfInput] tag If these options are not available, you can take advantage of the fact that the `CsrfToken` is exposed as an <>. @@ -763,7 +763,7 @@ Spring Security defers loading a new CSRF token by default, and additional work [NOTE] ==== -Refreshing the token after authentication success and logout success is required because the {security-api-url}org/springframework/security/web/csrf/CsrfAuthenticationStrategy.html[`CsrfAuthenticationStrategy`] and {security-api-url}org/springframework/security/web/csrf/CsrfLogoutHandler.html[`CsrfLogoutHandler`] will clear the previous token. +Refreshing the token after authentication success and logout success is required because the javadoc:org.springframework.security.web.csrf.CsrfAuthenticationStrategy[] and javadoc:org.springframework.security.web.csrf.CsrfLogoutHandler[] will clear the previous token. The client application will not be able to perform an unsafe HTTP request, such as a POST, without obtaining a fresh token. ==== @@ -1088,7 +1088,7 @@ This endpoint should be called to obtain a CSRF token when the application is la [NOTE] ==== -Refreshing the token after authentication success and logout success is required because the {security-api-url}org/springframework/security/web/csrf/CsrfAuthenticationStrategy.html[`CsrfAuthenticationStrategy`] and {security-api-url}org/springframework/security/web/csrf/CsrfLogoutHandler.html[`CsrfLogoutHandler`] will clear the previous token. +Refreshing the token after authentication success and logout success is required because the javadoc:org.springframework.security.web.csrf.CsrfAuthenticationStrategy[] and javadoc:org.springframework.security.web.csrf.CsrfLogoutHandler[] will clear the previous token. The client application will not be able to perform an unsafe HTTP request, such as a POST, without obtaining a fresh token. ==== diff --git a/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc b/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc index 4ab70e36695..29dd8fcf93c 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc @@ -28,7 +28,7 @@ The strategy is implemented in the class `AntPathRequestMatcher`, which uses Spr If you need a more powerful matching strategy, you can use regular expressions. The strategy implementation is then `RegexRequestMatcher`. -See the {security-api-url}/org/springframework/security/web/util/matcher/RegexRequestMatcher.html[Javadoc for this class] for more information. +See the javadoc:org.springframework.security.web.util.matcher.RegexRequestMatcher[] Javadoc for more information. In practice, we recommend that you use method security at your service layer, to control access to your application, rather than rely entirely on the use of security constraints defined at the web-application level. URLs change, and it is difficult to take into account all the possible URLs that an application might support and how requests might be manipulated. diff --git a/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc b/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc index d48b9042ab8..25753ff4bcd 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc @@ -153,12 +153,12 @@ This means that we are running our `Runnable` with the same user that was used t See the {security-api-url}index.html[Javadoc] for additional integrations with both the Java concurrent APIs and the Spring Task abstractions. They are self-explanatory once you understand the previous code. -* {security-api-url}org/springframework/security/concurrent/DelegatingSecurityContextCallable.html[`DelegatingSecurityContextCallable`] -* {security-api-url}org/springframework/security/concurrent/DelegatingSecurityContextExecutor.html[`DelegatingSecurityContextExecutor`] -* {security-api-url}org/springframework/security/concurrent/DelegatingSecurityContextExecutorService.html[`DelegatingSecurityContextExecutorService`] -* {security-api-url}org/springframework/security/concurrent/DelegatingSecurityContextRunnable.html[`DelegatingSecurityContextRunnable`] -* {security-api-url}org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorService.html[`DelegatingSecurityContextScheduledExecutorService`] -* {security-api-url}org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutor.html[`DelegatingSecurityContextSchedulingTaskExecutor`] -* {security-api-url}org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.html[`DelegatingSecurityContextAsyncTaskExecutor`] -* {security-api-url}org/springframework/security/task/DelegatingSecurityContextTaskExecutor.html[`DelegatingSecurityContextTaskExecutor`] -* {security-api-url}org/springframework/security/scheduling/DelegatingSecurityContextTaskScheduler.html[`DelegatingSecurityContextTaskScheduler`] +* javadoc:org.springframework.security.concurrent.DelegatingSecurityContextCallable[] +* javadoc:org.springframework.security.concurrent.DelegatingSecurityContextExecutor[] +* javadoc:org.springframework.security.concurrent.DelegatingSecurityContextExecutorService[] +* javadoc:org.springframework.security.concurrent.DelegatingSecurityContextRunnable[] +* javadoc:org.springframework.security.concurrent.DelegatingSecurityContextScheduledExecutorService[] +* javadoc:org.springframework.security.scheduling.DelegatingSecurityContextSchedulingTaskExecutor[] +* javadoc:org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor[] +* javadoc:org.springframework.security.task.DelegatingSecurityContextTaskExecutor[] +* javadoc:org.springframework.security.scheduling.DelegatingSecurityContextTaskScheduler[] diff --git a/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc index 538379459cc..d8f7eabd523 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc @@ -23,8 +23,8 @@ String json = mapper.writeValueAsString(context); ==== The following Spring Security modules provide Jackson support: -- spring-security-core ({security-api-url}org/springframework/security/jackson2/CoreJackson2Module.html[`CoreJackson2Module`]) -- spring-security-web ({security-api-url}org/springframework/security/web/jackson2/WebJackson2Module.html[`WebJackson2Module`], {security-api-url}org/springframework/security/web/jackson2/WebServletJackson2Module.html[`WebServletJackson2Module`], {security-api-url}org/springframework/security/web/server/jackson2/WebServerJackson2Module.html[`WebServerJackson2Module`]) -- <> ({security-api-url}org/springframework/security/oauth2/client/jackson2/OAuth2ClientJackson2Module.html[`OAuth2ClientJackson2Module`]) -- spring-security-cas ({security-api-url}org/springframework/security/cas/jackson2/CasJackson2Module.html[`CasJackson2Module`]) +- spring-security-core (javadoc:org.springframework.security.jackson2.CoreJackson2Module[]) +- spring-security-web (javadoc:org.springframework.security.web.jackson2.WebJackson2Module[], javadoc:org.springframework.security.web.jackson2.WebServletJackson2Module[], javadoc:org.springframework.security.web.server.jackson2.WebServerJackson2Module[]) +- <> (javadoc:org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module[]) +- spring-security-cas (javadoc:org.springframework.security.cas.jackson2.CasJackson2Module[]) ==== 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 a805132c280..aea9d358fcb 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 @@ -105,7 +105,7 @@ Xml:: == Bearer Token Propagation 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: +This is quite simple with javadoc:org.springframework.security.oauth2.server.resource.web.reactive.function.client.ServletBearerExchangeFilterFunction[], which you can see in the following example: [tabs] ====== @@ -134,7 +134,7 @@ fun rest(): WebClient { ---- ====== -When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential. +When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any javadoc:org.springframework.security.oauth2.core.AbstractOAuth2Token[] credential. Then, it will propagate that token in the `Authorization` header. For example: @@ -198,7 +198,7 @@ this.rest.get() In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain. [NOTE] -Unlike the {security-api-url}org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.html[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired. +Unlike the javadoc:org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired. To obtain this level of support, please use the OAuth 2.0 Client filter. === `RestTemplate` support @@ -259,7 +259,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. +Unlike the javadoc:org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager[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/client/index.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. [[oauth2resourceserver-bearertoken-failure]] diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc index 3a3eae8ea7a..0749008ebbc 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc @@ -30,7 +30,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is _Denied_ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates _Start Authentication_. -The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`], which sends a `WWW-Authenticate` header. +The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of javadoc:org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint[], which sends a `WWW-Authenticate` header. The `RequestCache` is typically a `NullRequestCache` that does not save the request, since the client is capable of replaying the requests it originally requested. When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token. 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 87dc8458ec2..8e2b765cd6e 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -86,7 +86,7 @@ From here, consider jumping to: Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7519[JWT] Authentication in servlet-based applications, like the one we just saw. -{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[`JwtAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> and <> to authenticate a JWT. +javadoc:org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider[] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> and <> to authenticate a JWT. Let's take a look at how `JwtAuthenticationProvider` works within Spring Security. The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. @@ -293,7 +293,7 @@ fun jwtDecoder(): JwtDecoder { ====== [NOTE] -Calling `{security-api-url}org/springframework/security/oauth2/jwt/JwtDecoders.html#fromIssuerLocation-java.lang.String-[JwtDecoders#fromIssuerLocation]` is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri. +Calling javadoc:org.springframework.security.oauth2.jwt.JwtDecoders#fromIssuerLocation-java.lang.String-[JwtDecoders#fromIssuerLocation] is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri. If the application doesn't expose a `JwtDecoder` bean, then Spring Boot will expose the above default one. @@ -879,7 +879,9 @@ public class DirectlyConfiguredJwkSetUri { .requestMatchers("/messages/**").access(hasScope("messages")) .anyRequest().authenticated() ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(Customizer.withDefaults()) + ); return http.build(); } } 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 fcd3b4f2c4c..90b64a01f46 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 @@ -68,7 +68,7 @@ Given an Opaque Token, Resource Server will 2. Inspect the response for an `{ 'active' : true }` attribute 3. Map each scope to an authority with the prefix `SCOPE_` -The resulting `Authentication#getPrincipal`, by default, is a Spring Security `{security-api-url}org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html[OAuth2AuthenticatedPrincipal]` object, and `Authentication#getName` maps to the token's `sub` property, if one is present. +The resulting `Authentication#getPrincipal`, by default, is a Spring Security javadoc:org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal[] object, and `Authentication#getName` maps to the token's `sub` property, if one is present. From here, you may want to jump to: @@ -82,7 +82,7 @@ From here, you may want to jump to: Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7662[opaque token] Authentication in servlet-based applications, like the one we just saw. -{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[`OpaqueTokenAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> to authenticate an opaque token. +javadoc:org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider[] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> to authenticate an opaque token. Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security. The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. @@ -204,7 +204,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(Customizer.withDefaults()) + ); return http.build(); } ---- @@ -564,7 +566,9 @@ public class MappedAuthorities { .requestMatchers("/messages/**").access(hasScope("messages")) .anyRequest().authenticated() ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(Customizer.withDefaults()) + ); 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 76f4a236146..4e0ec21d32e 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -87,7 +87,7 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit Kotlin:: + -[source,java,role="secondary"] +[source,kotlin,role="secondary"] ---- var relyingPartyRegistration: RelyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") @@ -96,7 +96,7 @@ var relyingPartyRegistration: RelyingPartyRegistration = // ... .wantAuthnRequestsSigned(false) } - .build(); + .build() ---- ====== @@ -141,7 +141,7 @@ var relyingPartyRegistration: RelyingPartyRegistration = ) } } - .build(); + .build() ---- ====== diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index 2ab0b7554c2..77c43da2dc1 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -19,7 +19,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is _Denied_ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates _Start Authentication_. -The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`], which redirects to <` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`. +The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of javadoc:org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint[], which redirects to <` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`. Alternatively, if you have <>, it first redirects to a picker page. image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `` using its configured <>. @@ -418,7 +418,7 @@ class MyCustomSecurityConfiguration { The preceding example requires the role of `USER` for any URL that starts with `/messages/`. [[servlet-saml2login-relyingpartyregistrationrepository]] -The second `@Bean` Spring Boot creates is a {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository`], which represents the asserting party and relying party metadata. +The second `@Bean` Spring Boot creates is a javadoc:org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository[], which represents the asserting party and relying party metadata. This includes such things as the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party. You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean. @@ -588,9 +588,60 @@ class MyCustomSecurityConfiguration { A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`. ==== +[[servlet-saml2login-relyingpartyregistrationrepository-caching]] +If you want your metadata to be refreshable on a periodic basis, you can wrap your repository in `CachingRelyingPartyRegistrationRepository` like so: + +.Caching Relying Party Registration Repository +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableWebSecurity +public class MyCustomSecurityConfiguration { + @Bean + public RelyingPartyRegistrationRepository registrations(CacheManager cacheManager) { + Supplier delegate = () -> + new InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistrations + .fromMetadataLocation("https://idp.example.org/ap/metadata") + .registrationId("ap").build()); + CachingRelyingPartyRegistrationRepository registrations = + new CachingRelyingPartyRegistrationRepository(delegate); + registrations.setCache(cacheManager.getCache("my-cache-name")); + return registrations; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableWebSecurity +class MyCustomSecurityConfiguration { + @Bean + fun registrations(cacheManager: CacheManager): RelyingPartyRegistrationRepository { + val delegate = Supplier { + InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistrations + .fromMetadataLocation("https://idp.example.org/ap/metadata") + .registrationId("ap").build()) + } + val registrations = CachingRelyingPartyRegistrationRepository(delegate) + registrations.setCache(cacheManager.getCache("my-cache-name")) + return registrations + } +} +---- +====== + +In this way, the set of `RelyingPartyRegistration`s will refresh based on {spring-framework-reference-url}integration/cache/store-configuration.html[the cache's eviction schedule]. + [[servlet-saml2login-relyingpartyregistration]] == RelyingPartyRegistration -A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`] +A javadoc:org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration[] instance represents a link between an relying party and an asserting party's metadata. In a `RelyingPartyRegistration`, you can provide relying party metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads. @@ -949,12 +1000,12 @@ Collection registrations = RelyingPartyRegistrations .entityId("https://example.org/saml2/sp") .build() ) - .collect(Collectors.toList())); + .collect(Collectors.toList()); ---- Kotlin:: + -[source,java,role="secondary"] +[source,kotlin,role="secondary"] ---- var registrations: Collection = RelyingPartyRegistrations .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml") @@ -964,7 +1015,7 @@ var registrations: Collection = RelyingPartyRegistrati .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso") .build() } - .collect(Collectors.toList())); + .collect(Collectors.toList()) ---- ====== diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 34038df70c6..97e155cdd30 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -189,28 +189,28 @@ Next, let's see the architectural components that Spring Security uses to suppor For RP-initiated logout: image:{icondir}/number_1.png[] Spring Security executes its xref:servlet/authentication/logout.adoc#logout-architecture[logout flow], calling its ``LogoutHandler``s to invalidate the session and perform other cleanup. -It then invokes the {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutSuccessHandler.html[`Saml2RelyingPartyInitiatedLogoutSuccessHandler`]. +It then invokes the javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler[]. image:{icondir}/number_2.png[] The logout success handler uses an instance of -{security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.html[`Saml2LogoutRequestResolver`] to create, sign, and serialize a ``. +javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver[] to create, sign, and serialize a ``. It uses the keys and configuration from the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] that is associated with the current `Saml2AuthenticatedPrincipal`. Then, it redirect-POSTs the `` to the asserting party SLO endpoint The browser hands control over to the asserting party. If the asserting party redirects back (which it may not), then the application proceeds to step image:{icondir}/number_3.png[]. -image:{icondir}/number_3.png[] The {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.html[`Saml2LogoutResponseFilter`] deserializes, verifies, and processes the `` with its {security-api-url}org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponseValidator.html[`Saml2LogoutResponseValidator`]. +image:{icondir}/number_3.png[] The javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter[] deserializes, verifies, and processes the `` with its javadoc:org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator[]. image:{icondir}/number_4.png[] If valid, then it completes the local logout flow by redirecting to `/login?logout`, or whatever has been configured. If invalid, then it responds with a 400. For AP-initiated logout: -image:{icondir}/number_1.png[] The {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.html[`Saml2LogoutRequestFilter`] deserializes, verifies, and processes the `` with its {security-api-url}org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequestValidator.html[`Saml2LogoutRequestValidator`]. +image:{icondir}/number_1.png[] The javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter[] deserializes, verifies, and processes the `` with its javadoc:org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator[]. image:{icondir}/number_2.png[] If valid, then the filter calls the configured ``LogoutHandler``s, invalidating the session and performing other cleanup. -image:{icondir}/number_3.png[] It uses a {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.html[`Saml2LogoutResponseResolver`] to create, sign and serialize a ``. +image:{icondir}/number_3.png[] It uses a javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver[] to create, sign and serialize a ``. It uses the keys and configuration from the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] derived from the endpoint or from the contents of the ``. Then, it redirect-POSTs the `` to the asserting party SLO endpoint. diff --git a/docs/modules/ROOT/pages/servlet/test/mockmvc/oauth2.adoc b/docs/modules/ROOT/pages/servlet/test/mockmvc/oauth2.adoc index 562ee2d7c45..581f49adefa 100644 --- a/docs/modules/ROOT/pages/servlet/test/mockmvc/oauth2.adoc +++ b/docs/modules/ROOT/pages/servlet/test/mockmvc/oauth2.adoc @@ -964,7 +964,7 @@ mvc.get("/endpoint") { ---- ====== -You can also specify a complete `Jwt`, for which `{security-api-url}org/springframework/security/oauth2/jwt/Jwt.Builder.html[Jwt.Builder]` comes quite handy: +You can also specify a complete `Jwt`, for which javadoc:org.springframework.security.oauth2.jwt.Jwt$Builder[] comes quite handy: [tabs] ====== diff --git a/docs/package.json b/docs/package.json index d4fa935fa78..24797cedba8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,10 +1,10 @@ { "dependencies": { - "antora": "3.2.0-alpha.4", + "antora": "3.2.0-alpha.5", "@antora/atlas-extension": "1.0.0-alpha.2", "@antora/collector-extension": "1.0.0-alpha.4", "@asciidoctor/tabs": "1.0.0-beta.6", - "@springio/antora-extensions": "1.11.1", - "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + "@springio/antora-extensions": "1.12.0", + "@springio/asciidoctor-extensions": "1.0.0-alpha.11" } } diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 2f7cf9c7941..fdd01e80b0d 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -15,11 +15,22 @@ antora { ] } +tasks.register("syncAntoraAttachments", Sync) { + group = 'Documentation' + description = 'Syncs the Antora attachments' + from project.provider( { project.tasks.api.outputs } ) + into project.layout.buildDirectory.dir('generated-antora-resources/modules/ROOT/assets/attachments/api/java') +} + tasks.named("generateAntoraYml") { asciidocAttributes = project.provider( { generateAttributes() } ) asciidocAttributes.putAll(providers.provider( { resolvedVersions(project.configurations.testRuntimeClasspath) })) } +tasks.register("generateAntoraResources") { + dependsOn 'generateAntoraYml', 'syncAntoraAttachments' +} + dependencies { testImplementation platform(project(':spring-security-dependencies')) testImplementation 'com.unboundid:unboundid-ldapsdk' @@ -44,7 +55,7 @@ def generateAttributes() { def securityApiUrl = "$securityDocsUrl/api/" def securityReferenceUrl = "$securityDocsUrl/reference/html5/" def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/" - def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/" + def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/reference/$springFrameworkVersion/" def springBootReferenceUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/reference/html/" def springBootApiUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/api/" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec10a508b99..73a8d274216 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,10 +5,10 @@ io-spring-javaformat = "0.0.42" io-spring-nohttp = "0.0.11" jakarta-websocket = "2.1.1" org-apache-directory-server = "1.5.5" -org-apache-maven-resolver = "1.9.20" +org-apache-maven-resolver = "1.9.21" org-aspectj = "1.9.22.1" org-bouncycastle = "1.78.1" -org-eclipse-jetty = "11.0.21" +org-eclipse-jetty = "11.0.22" org-jetbrains-kotlin = "1.9.24" org-jetbrains-kotlinx = "1.8.1" org-mockito = "5.11.0" @@ -17,7 +17,7 @@ org-springframework = "6.2.0-M4" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.6" -com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.1" +com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.2" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.3" @@ -26,9 +26,9 @@ com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebser com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" } com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11" commons-collections = "commons-collections:commons-collections:3.2.2" -io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.7" +io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.8" io-mockk = "io.mockk:mockk:1.13.11" -io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.7" +io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.8" io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" } io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" } io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" } @@ -73,7 +73,7 @@ org-hsqldb = "org.hsqldb:hsqldb:2.7.3" org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24" org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } -org-junit-junit-bom = "org.junit:junit-bom:5.10.2" +org-junit-junit-bom = "org.junit:junit-bom:5.10.3" org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" } org-opensaml-opensaml-core = { module = "org.opensaml:opensaml-core", version.ref = "org-opensaml" } org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" } @@ -82,7 +82,7 @@ org-python-jython = { module = "org.python:jython", version = "2.5.3" } org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit3-driver:4.20.0" org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.20.0" org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59" -org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1" +org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3" org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36" org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.13" org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.1" @@ -98,7 +98,7 @@ org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2" io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.3.0" org-gretty-gretty = "org.gretty:gretty:4.1.4" com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0" -com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.13" +com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14" org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1" org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.20" org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969" 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 4668d371745..ced26c10862 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 @@ -69,10 +69,11 @@ public class ActiveDirectoryLdapAuthenticationProviderTests { ActiveDirectoryLdapAuthenticationProvider provider; UsernamePasswordAuthenticationToken joe = UsernamePasswordAuthenticationToken.unauthenticated("joe", "password"); - + DirContext ctx; @BeforeEach public void setUp() { this.provider = new ActiveDirectoryLdapAuthenticationProvider("mydomain.eu", "ldap://192.168.1.200/"); + ctx = mock(DirContext.class); } @Test @@ -90,8 +91,6 @@ public void successfulAuthenticationProducesExpectedAuthorities() throws Excepti @Test public void customSearchFilterIsUsedForSuccessfulAuthentication() throws Exception { String customSearchFilter = "(&(objectClass=user)(sAMAccountName={0}))"; - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); DirContextAdapter dca = new DirContextAdapter(); SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes()); given(ctx.search(any(Name.class), eq(customSearchFilter), any(Object[].class), any(SearchControls.class))) @@ -107,8 +106,6 @@ public void customSearchFilterIsUsedForSuccessfulAuthentication() throws Excepti @Test public void defaultSearchFilter() throws Exception { final String defaultSearchFilter = "(&(objectClass=user)(userPrincipalName={0}))"; - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); DirContextAdapter dca = new DirContextAdapter(); SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes()); given(ctx.search(any(Name.class), eq(defaultSearchFilter), any(Object[].class), any(SearchControls.class))) @@ -126,8 +123,6 @@ public void defaultSearchFilter() throws Exception { public void bindPrincipalAndUsernameUsed() throws Exception { final String defaultSearchFilter = "(&(objectClass=user)(userPrincipalName={0}))"; ArgumentCaptor captor = ArgumentCaptor.forClass(Object[].class); - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); DirContextAdapter dca = new DirContextAdapter(); SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes()); given(ctx.search(any(Name.class), eq(defaultSearchFilter), captor.capture(), any(SearchControls.class))) @@ -153,8 +148,6 @@ public void setSearchFilterEmpty() { @Test public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws Exception { this.provider = new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/"); - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); DirContextAdapter dca = new DirContextAdapter(); SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes()); given(ctx.search(eq(LdapNameBuilder.newInstance("DC=mydomain,DC=eu").build()), any(String.class), @@ -167,8 +160,6 @@ public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws @Test public void failedUserSearchCausesBadCredentials() throws Exception { - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); given(ctx.search(any(Name.class), any(String.class), any(Object[].class), any(SearchControls.class))) .willThrow(new NameNotFoundException()); this.provider.contextFactory = createContextFactoryReturning(ctx); @@ -178,8 +169,6 @@ public void failedUserSearchCausesBadCredentials() throws Exception { // SEC-2017 @Test public void noUserSearchCausesUsernameNotFound() throws Exception { - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); given(ctx.search(any(Name.class), any(String.class), any(Object[].class), any(SearchControls.class))) .willReturn(new EmptyEnumeration<>()); this.provider.contextFactory = createContextFactoryReturning(ctx); @@ -196,8 +185,6 @@ public void sec2500PreventAnonymousBind() { @Test @SuppressWarnings("unchecked") public void duplicateUserSearchCausesError() throws Exception { - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); NamingEnumeration searchResults = mock(NamingEnumeration.class); given(searchResults.hasMore()).willReturn(true, true, false); SearchResult searchResult = mock(SearchResult.class); @@ -209,7 +196,6 @@ public void duplicateUserSearchCausesError() throws Exception { assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) .isThrownBy(() -> this.provider.authenticate(this.joe)); } - static final String msg = "[LDAP: error code 49 - 80858585: LdapErr: DSID-DECAFF0, comment: AcceptSecurityContext error, data "; @Test @@ -357,8 +343,6 @@ DirContext createContext(Hashtable env) { private void checkAuthentication(String rootDn, ActiveDirectoryLdapAuthenticationProvider provider) throws NamingException { - DirContext ctx = mock(DirContext.class); - given(ctx.getNameInNamespace()).willReturn(""); DirContextAdapter dca = new DirContextAdapter(); SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes()); @SuppressWarnings("deprecation") diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/CachingRelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/CachingRelyingPartyRegistrationRepository.java new file mode 100644 index 00000000000..bfc39e486fb --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/CachingRelyingPartyRegistrationRepository.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import java.util.Iterator; +import java.util.Spliterator; +import java.util.concurrent.Callable; +import java.util.function.Consumer; + +import org.springframework.cache.Cache; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.util.Assert; + +/** + * An {@link IterableRelyingPartyRegistrationRepository} that lazily queries and caches + * metadata from a backing {@link IterableRelyingPartyRegistrationRepository}. Delegates + * caching policies to Spring Cache. + * + * @author Josh Cummings + * @since 6.4 + */ +public final class CachingRelyingPartyRegistrationRepository implements IterableRelyingPartyRegistrationRepository { + + private final Callable registrationLoader; + + private Cache cache = new ConcurrentMapCache("registrations"); + + public CachingRelyingPartyRegistrationRepository(Callable loader) { + this.registrationLoader = loader; + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return registrations().iterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public RelyingPartyRegistration findByRegistrationId(String registrationId) { + return registrations().findByRegistrationId(registrationId); + } + + @Override + public RelyingPartyRegistration findUniqueByAssertingPartyEntityId(String entityId) { + return registrations().findUniqueByAssertingPartyEntityId(entityId); + } + + @Override + public void forEach(Consumer action) { + registrations().forEach(action); + } + + @Override + public Spliterator spliterator() { + return registrations().spliterator(); + } + + private IterableRelyingPartyRegistrationRepository registrations() { + return this.cache.get("registrations", this.registrationLoader); + } + + /** + * Use this cache for the completed {@link RelyingPartyRegistration} instances. + * + *

+ * Defaults to {@link ConcurrentMapCache}, meaning that the registrations are cached + * without expiry. To turn off the cache, use + * {@link org.springframework.cache.support.NoOpCache}. + * @param cache the {@link Cache} to use + */ + public void setCache(Cache cache) { + Assert.notNull(cache, "cache cannot be null"); + this.cache = cache; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java index 738f00952fa..01f4778027d 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ * @author Josh Cummings * @since 5.2 */ -public class InMemoryRelyingPartyRegistrationRepository - implements RelyingPartyRegistrationRepository, Iterable { +public class InMemoryRelyingPartyRegistrationRepository implements IterableRelyingPartyRegistrationRepository { private final Map byRegistrationId; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/IterableRelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/IterableRelyingPartyRegistrationRepository.java new file mode 100644 index 00000000000..c41261f6a11 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/IterableRelyingPartyRegistrationRepository.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +/** + * An interface that simplifies APIs which require the + * {@link RelyingPartyRegistrationRepository} to also be {@link Iterable} + * + * @author Josh Cummings + * @since 6.4 + * @see InMemoryRelyingPartyRegistrationRepository + * @see CachingRelyingPartyRegistrationRepository + */ +public interface IterableRelyingPartyRegistrationRepository + extends RelyingPartyRegistrationRepository, Iterable { + +} 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 index 25d5738a6c1..1f447f95ff0 100644 --- 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 @@ -16,12 +16,25 @@ package org.springframework.security.saml2.provider.service.registration; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.ext.saml2alg.SigningMethod; import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.Extensions; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.SingleLogoutService; +import org.opensaml.saml.saml2.metadata.SingleSignOnService; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.core.Saml2X509Credential; /** @@ -62,7 +75,125 @@ public EntityDescriptor getEntityDescriptor() { * for further configurations */ public static OpenSamlAssertingPartyDetails.Builder withEntityDescriptor(EntityDescriptor entity) { - return new OpenSamlAssertingPartyDetails.Builder(entity); + IDPSSODescriptor idpssoDescriptor = entity.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + if (idpssoDescriptor == null) { + throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); + } + List verification = new ArrayList<>(); + List encryption = new ArrayList<>(); + for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { + if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + } + if (verification.isEmpty()) { + throw new Saml2Exception( + "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); + } + OpenSamlAssertingPartyDetails.Builder builder = new OpenSamlAssertingPartyDetails.Builder(entity) + .entityId(entity.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.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())); + } + if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) { + throw new Saml2Exception( + "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); + } + for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { + Saml2MessageBinding binding; + if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { + binding = Saml2MessageBinding.POST; + } + else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { + binding = Saml2MessageBinding.REDIRECT; + } + else { + continue; + } + builder.singleSignOnServiceLocation(singleSignOnService.getLocation()).singleSignOnServiceBinding(binding); + break; + } + for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) { + Saml2MessageBinding binding; + if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { + binding = Saml2MessageBinding.POST; + } + else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { + binding = Saml2MessageBinding.REDIRECT; + } + else { + continue; + } + String responseLocation = (singleLogoutService.getResponseLocation() == null) + ? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation(); + builder.singleLogoutServiceLocation(singleLogoutService.getLocation()) + .singleLogoutServiceResponseLocation(responseLocation) + .singleLogoutServiceBinding(binding); + break; + } + return builder; + } + + private static List certificates(KeyDescriptor keyDescriptor) { + try { + return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); + } + catch (CertificateException ex) { + throw new Saml2Exception(ex); + } + } + + private static List signingMethods(IDPSSODescriptor idpssoDescriptor) { + Extensions extensions = idpssoDescriptor.getExtensions(); + List result = signingMethods(extensions); + if (!result.isEmpty()) { + return result; + } + EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent(); + extensions = descriptor.getExtensions(); + return signingMethods(extensions); + } + + private static List signingMethods(Extensions extensions) { + if (extensions != null) { + return (List) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); + } + return new ArrayList<>(); + } + + @Override + public OpenSamlAssertingPartyDetails.Builder mutate() { + return new OpenSamlAssertingPartyDetails.Builder(this.descriptor).entityId(getEntityId()) + .wantAuthnRequestsSigned(getWantAuthnRequestsSigned()) + .signingAlgorithms((algorithms) -> algorithms.addAll(getSigningAlgorithms())) + .verificationX509Credentials((c) -> c.addAll(getVerificationX509Credentials())) + .encryptionX509Credentials((c) -> c.addAll(getEncryptionX509Credentials())) + .singleSignOnServiceLocation(getSingleSignOnServiceLocation()) + .singleSignOnServiceBinding(getSingleSignOnServiceBinding()) + .singleLogoutServiceLocation(getSingleLogoutServiceLocation()) + .singleLogoutServiceResponseLocation(getSingleLogoutServiceResponseLocation()) + .singleLogoutServiceBinding(getSingleLogoutServiceBinding()); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java deleted file mode 100644 index a3ab016d0ee..00000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.saml2.provider.service.registration; - -import java.io.InputStream; -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; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.io.Unmarshaller; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.ext.saml2alg.SigningMethod; -import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.saml.saml2.metadata.Extensions; -import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; -import org.opensaml.saml.saml2.metadata.KeyDescriptor; -import org.opensaml.saml.saml2.metadata.SingleLogoutService; -import org.opensaml.saml.saml2.metadata.SingleSignOnService; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.OpenSamlInitializationService; -import org.springframework.security.saml2.core.Saml2X509Credential; - -class OpenSamlMetadataRelyingPartyRegistrationConverter { - - static { - OpenSamlInitializationService.initialize(); - } - - private final XMLObjectProviderRegistry registry; - - private final ParserPool parserPool; - - /** - * Creates a {@link OpenSamlMetadataRelyingPartyRegistrationConverter} - */ - OpenSamlMetadataRelyingPartyRegistrationConverter() { - this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = this.registry.getParserPool(); - } - - OpenSamlRelyingPartyRegistration.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"); - } - List verification = new ArrayList<>(); - List encryption = new ArrayList<>(); - for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { - if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - } - if (verification.isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); - } - OpenSamlRelyingPartyRegistration.Builder builder = OpenSamlRelyingPartyRegistration - .withAssertingPartyEntityDescriptor(descriptor) - .assertingPartyDetails((party) -> party.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()))); - } - if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); - } - for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { - Saml2MessageBinding binding; - if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { - binding = Saml2MessageBinding.POST; - } - else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { - binding = Saml2MessageBinding.REDIRECT; - } - else { - continue; - } - builder - .assertingPartyDetails((party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) - .singleSignOnServiceBinding(binding)); - break; - } - for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) { - Saml2MessageBinding binding; - if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { - binding = Saml2MessageBinding.POST; - } - else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { - binding = Saml2MessageBinding.REDIRECT; - } - else { - continue; - } - String responseLocation = (singleLogoutService.getResponseLocation() == null) - ? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation(); - builder - .assertingPartyDetails((party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation()) - .singleLogoutServiceResponseLocation(responseLocation) - .singleLogoutServiceBinding(binding)); - break; - } - - return builder; - } - - 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()) { - if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) { - builders.add(convert(descriptor)); - } - } - if (builders.isEmpty()) { - throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements"); - } - return builders; - } - if (xmlObject instanceof EntityDescriptor) { - EntityDescriptor descriptor = (EntityDescriptor) xmlObject; - return Arrays.asList(convert(descriptor)); - } - throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass()); - } - - private List certificates(KeyDescriptor keyDescriptor) { - try { - return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); - } - catch (CertificateException ex) { - throw new Saml2Exception(ex); - } - } - - private List signingMethods(IDPSSODescriptor idpssoDescriptor) { - Extensions extensions = idpssoDescriptor.getExtensions(); - List result = signingMethods(extensions); - if (!result.isEmpty()) { - return result; - } - EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent(); - extensions = descriptor.getExtensions(); - return signingMethods(extensions); - } - - private XMLObject xmlObject(InputStream inputStream) { - Document document = document(inputStream); - Element element = document.getDocumentElement(); - Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); - if (unmarshaller == null) { - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); - } - try { - return unmarshaller.unmarshall(element); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private Document document(InputStream inputStream) { - try { - return this.parserPool.parse(inputStream); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private List signingMethods(Extensions extensions) { - if (extensions != null) { - return (List) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); - } - return new ArrayList<>(); - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java new file mode 100644 index 00000000000..b7efc22c407 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; + +final class OpenSamlMetadataUtils { + + static { + OpenSamlInitializationService.initialize(); + } + + private OpenSamlMetadataUtils() { + + } + + static Collection descriptors(InputStream metadata) { + XMLObject object = xmlObject(metadata); + if (object instanceof EntityDescriptor descriptor) { + return Collections.singleton(descriptor); + } + if (object instanceof EntitiesDescriptor descriptors) { + return descriptors.getEntityDescriptors(); + } + throw new Saml2Exception("Unsupported element type: " + object.getClass().getName()); + } + + static XMLObject xmlObject(InputStream inputStream) { + Document document = document(inputStream); + Element element = document.getDocumentElement(); + Unmarshaller unmarshaller = XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element); + if (unmarshaller == null) { + throw new Saml2Exception("Unsupported element of type " + element.getTagName()); + } + try { + return unmarshaller.unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + static Document document(InputStream inputStream) { + try { + return XMLObjectProviderRegistrySupport.getParserPool().parse(inputStream); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java index ce9061ad4a3..9b3f26dc3a9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java @@ -29,7 +29,19 @@ * * @author Josh Cummings * @since 6.1 + * @deprecated This class no longer is needed in order to transmit the + * {@link EntityDescriptor} to {@link OpenSamlAssertingPartyDetails}. Instead of doing: + *

+ * 	if (registration instanceof OpenSamlRelyingPartyRegistration openSamlRegistration) {
+ * 	    EntityDescriptor descriptor = openSamlRegistration.getAssertingPartyDetails.getEntityDescriptor();
+ * 	}
+ * 
do instead:
+ * 	if (registration.getAssertingPartyDetails() instanceof openSamlAssertingPartyDetails) {
+ * 	    EntityDescriptor descriptor = openSamlAssertingPartyDetails.getEntityDescriptor();
+ * 	}
+ * 
*/ +@Deprecated public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration { OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) { @@ -47,7 +59,7 @@ public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistra @Override public OpenSamlRelyingPartyRegistration.Builder mutate() { OpenSamlAssertingPartyDetails party = getAssertingPartyDetails(); - return withAssertingPartyEntityDescriptor(party.getEntityDescriptor()).registrationId(getRegistrationId()) + return new Builder(party).registrationId(getRegistrationId()) .entityId(getEntityId()) .signingX509Credentials((c) -> c.addAll(getSigningX509Credentials())) .decryptionX509Credentials((c) -> c.addAll(getDecryptionX509Credentials())) @@ -57,18 +69,7 @@ public OpenSamlRelyingPartyRegistration.Builder mutate() { .singleLogoutServiceResponseLocation(getSingleLogoutServiceResponseLocation()) .singleLogoutServiceBindings((c) -> c.addAll(getSingleLogoutServiceBindings())) .nameIdFormat(getNameIdFormat()) - .authnRequestsSigned(isAuthnRequestsSigned()) - .assertingPartyDetails((assertingParty) -> ((OpenSamlAssertingPartyDetails.Builder) assertingParty) - .entityId(party.getEntityId()) - .wantAuthnRequestsSigned(party.getWantAuthnRequestsSigned()) - .signingAlgorithms((algorithms) -> algorithms.addAll(party.getSigningAlgorithms())) - .verificationX509Credentials((c) -> c.addAll(party.getVerificationX509Credentials())) - .encryptionX509Credentials((c) -> c.addAll(party.getEncryptionX509Credentials())) - .singleSignOnServiceLocation(party.getSingleSignOnServiceLocation()) - .singleSignOnServiceBinding(party.getSingleSignOnServiceBinding()) - .singleLogoutServiceLocation(party.getSingleLogoutServiceLocation()) - .singleLogoutServiceResponseLocation(party.getSingleLogoutServiceResponseLocation()) - .singleLogoutServiceBinding(party.getSingleLogoutServiceBinding())); + .authnRequestsSigned(isAuthnRequestsSigned()); } /** @@ -100,6 +101,10 @@ private Builder(EntityDescriptor entityDescriptor) { super(entityDescriptor.getEntityID(), OpenSamlAssertingPartyDetails.withEntityDescriptor(entityDescriptor)); } + Builder(OpenSamlAssertingPartyDetails details) { + super(details.getEntityDescriptor().getEntityID(), details.mutate()); + } + @Override public Builder registrationId(String id) { return (Builder) super.registrationId(id); 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 3d4069ab8f0..4437261e7fb 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,15 +62,6 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter OpenSamlInitializationService.initialize(); } - private final OpenSamlMetadataRelyingPartyRegistrationConverter converter; - - /** - * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter} - */ - public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() { - this.converter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); - } - @Override public boolean canRead(Class clazz, MediaType mediaType) { return RelyingPartyRegistration.Builder.class.isAssignableFrom(clazz); @@ -89,7 +80,7 @@ public List getSupportedMediaTypes() { @Override public RelyingPartyRegistration.Builder read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - return this.converter.convert(inputMessage.getBody()).iterator().next(); + return RelyingPartyRegistrations.fromMetadata(inputMessage.getBody()); } @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 9e6f1b75332..87cfea754ec 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 @@ -139,8 +139,7 @@ protected RelyingPartyRegistration(String registrationId, String entityId, Strin * @since 6.1 */ public Builder mutate() { - AssertingPartyDetails party = this.assertingPartyDetails; - return withRegistrationId(this.registrationId).entityId(this.entityId) + return new Builder(this.registrationId, this.assertingPartyDetails.mutate()).entityId(this.entityId) .signingX509Credentials((c) -> c.addAll(this.signingX509Credentials)) .decryptionX509Credentials((c) -> c.addAll(this.decryptionX509Credentials)) .assertionConsumerServiceLocation(this.assertionConsumerServiceLocation) @@ -149,17 +148,7 @@ public Builder mutate() { .singleLogoutServiceResponseLocation(this.singleLogoutServiceResponseLocation) .singleLogoutServiceBindings((c) -> c.addAll(this.singleLogoutServiceBindings)) .nameIdFormat(this.nameIdFormat) - .authnRequestsSigned(this.authnRequestsSigned) - .assertingPartyDetails((assertingParty) -> assertingParty.entityId(party.getEntityId()) - .wantAuthnRequestsSigned(party.getWantAuthnRequestsSigned()) - .signingAlgorithms((algorithms) -> algorithms.addAll(party.getSigningAlgorithms())) - .verificationX509Credentials((c) -> c.addAll(party.getVerificationX509Credentials())) - .encryptionX509Credentials((c) -> c.addAll(party.getEncryptionX509Credentials())) - .singleSignOnServiceLocation(party.getSingleSignOnServiceLocation()) - .singleSignOnServiceBinding(party.getSingleSignOnServiceBinding()) - .singleLogoutServiceLocation(party.getSingleLogoutServiceLocation()) - .singleLogoutServiceResponseLocation(party.getSingleLogoutServiceResponseLocation()) - .singleLogoutServiceBinding(party.getSingleLogoutServiceBinding())); + .authnRequestsSigned(this.authnRequestsSigned); } /** @@ -346,17 +335,7 @@ public static Builder withRegistrationId(String 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())); + return new Builder(assertingPartyDetails.getEntityId(), assertingPartyDetails.mutate()); } /** @@ -592,6 +571,19 @@ public Saml2MessageBinding getSingleLogoutServiceBinding() { return this.singleLogoutServiceBinding; } + public AssertingPartyDetails.Builder mutate() { + return new AssertingPartyDetails.Builder().entityId(this.entityId) + .wantAuthnRequestsSigned(this.wantAuthnRequestsSigned) + .signingAlgorithms((algorithms) -> algorithms.addAll(this.signingAlgorithms)) + .verificationX509Credentials((c) -> c.addAll(this.verificationX509Credentials)) + .encryptionX509Credentials((c) -> c.addAll(this.encryptionX509Credentials)) + .singleSignOnServiceLocation(this.singleSignOnServiceLocation) + .singleSignOnServiceBinding(this.singleSignOnServiceBinding) + .singleLogoutServiceLocation(this.singleLogoutServiceLocation) + .singleLogoutServiceResponseLocation(this.singleLogoutServiceResponseLocation) + .singleLogoutServiceBinding(this.singleLogoutServiceBinding); + } + public static class Builder { private String entityId; 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 d7382eafd33..8c72db6f9c6 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 @@ -18,8 +18,12 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; + import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.security.saml2.Saml2Exception; @@ -34,8 +38,6 @@ */ public final class RelyingPartyRegistrations { - private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); - private static final ResourceLoader resourceLoader = new DefaultResourceLoader(); private RelyingPartyRegistrations() { @@ -45,8 +47,8 @@ private RelyingPartyRegistrations() { * Return a {@link RelyingPartyRegistration.Builder} 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: + * Valid locations can be classpath- or file-based or they can be HTTPS endpoints. + * Some valid endpoints might include: * *
 	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
@@ -69,8 +71,8 @@ private RelyingPartyRegistrations() {
 	 * 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 metadataLocation The classpath- or file-based locations or HTTP endpoints of
-	 * the asserting party metadata file
+	 * @param metadataLocation The classpath- or file-based locations or HTTPS endpoints
+	 * of the asserting party metadata file
 	 * @return the {@link RelyingPartyRegistration.Builder} for further configuration
 	 */
 	public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
@@ -130,8 +132,8 @@ public static RelyingPartyRegistration.Builder fromMetadata(InputStream source)
 	 * 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:
+	 * Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
+	 * Some valid endpoints might include:
 	 *
 	 * 
 	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
@@ -155,7 +157,7 @@ public static RelyingPartyRegistration.Builder fromMetadata(InputStream source)
 	 * 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
+	 * @param location The classpath- or file-based locations or HTTPS endpoints of the
 	 * asserting party metadata file
 	 * @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
 	 * further configuration
@@ -213,7 +215,19 @@ public static Collection collectionFromMetadat
 	 * @since 5.7
 	 */
 	public static Collection collectionFromMetadata(InputStream source) {
-		return relyingPartyRegistrationConverter.convert(source);
+		Collection builders = new ArrayList<>();
+		for (EntityDescriptor descriptor : OpenSamlMetadataUtils.descriptors(source)) {
+			if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
+				OpenSamlAssertingPartyDetails assertingParty = OpenSamlAssertingPartyDetails
+					.withEntityDescriptor(descriptor)
+					.build();
+				builders.add(new OpenSamlRelyingPartyRegistration.Builder(assertingParty));
+			}
+		}
+		if (builders.isEmpty()) {
+			throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
+		}
+		return builders;
 	}
 
 }
diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java
index 96850d36616..79dcb34dcc2 100644
--- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/metadata/RequestMatcherMetadataResponseResolver.java
@@ -30,6 +30,7 @@
 import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
 import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
 import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
+import org.springframework.security.saml2.provider.service.registration.IterableRelyingPartyRegistrationRepository;
 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.RelyingPartyRegistrationPlaceholderResolvers;
@@ -105,6 +106,9 @@ public Saml2MetadataResponse resolve(HttpServletRequest request) {
 		if (response != null) {
 			return response;
 		}
+		if (this.registrations instanceof IterableRelyingPartyRegistrationRepository iterable) {
+			return responseByIterable(request, iterable);
+		}
 		if (this.registrations instanceof Iterable) {
 			Iterable registrations = (Iterable) this.registrations;
 			return responseByIterable(request, registrations);
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/CachingRelyingPartyRegistrationRepositoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/CachingRelyingPartyRegistrationRepositoryTests.java
new file mode 100644
index 00000000000..7e4d57d444e
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/CachingRelyingPartyRegistrationRepositoryTests.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.registration;
+
+import java.util.concurrent.Callable;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.cache.Cache;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests for {@link CachingRelyingPartyRegistrationRepository}
+ */
+@ExtendWith(MockitoExtension.class)
+public class CachingRelyingPartyRegistrationRepositoryTests {
+
+	@Mock
+	Callable> callable;
+
+	@InjectMocks
+	CachingRelyingPartyRegistrationRepository registrations;
+
+	@Test
+	public void iteratorWhenResolvableThenPopulatesCache() throws Exception {
+		given(this.callable.call()).willReturn(mock(IterableRelyingPartyRegistrationRepository.class));
+		this.registrations.iterator();
+		verify(this.callable).call();
+		this.registrations.iterator();
+		verifyNoMoreInteractions(this.callable);
+	}
+
+	@Test
+	public void iteratorWhenExceptionThenPropagates() throws Exception {
+		given(this.callable.call()).willThrow(IllegalStateException.class);
+		assertThatExceptionOfType(Cache.ValueRetrievalException.class).isThrownBy(this.registrations::iterator)
+			.withCauseInstanceOf(IllegalStateException.class);
+	}
+
+	@Test
+	public void findByRegistrationIdWhenResolvableThenPopulatesCache() throws Exception {
+		given(this.callable.call()).willReturn(mock(IterableRelyingPartyRegistrationRepository.class));
+		this.registrations.findByRegistrationId("id");
+		verify(this.callable).call();
+		this.registrations.findByRegistrationId("id");
+		verifyNoMoreInteractions(this.callable);
+	}
+
+	@Test
+	public void findUniqueByAssertingPartyEntityIdWhenResolvableThenPopulatesCache() throws Exception {
+		given(this.callable.call()).willReturn(mock(IterableRelyingPartyRegistrationRepository.class));
+		this.registrations.findUniqueByAssertingPartyEntityId("id");
+		verify(this.callable).call();
+		this.registrations.findUniqueByAssertingPartyEntityId("id");
+		verifyNoMoreInteractions(this.callable);
+	}
+
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java
deleted file mode 100644
index ee270c86727..00000000000
--- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.saml2.provider.service.registration;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.Base64;
-import java.util.stream.Collectors;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.opensaml.saml.saml2.metadata.EntityDescriptor;
-import org.opensaml.xmlsec.signature.support.SignatureConstants;
-
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.security.saml2.Saml2Exception;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-
-public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
-
-	private static final String CERTIFICATE = "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";
-
-	private static final String ENTITIES_DESCRIPTOR_TEMPLATE = "\n%s";
-
-	private static final String ENTITY_DESCRIPTOR_TEMPLATE = "\n%s"
-			+ "";
-
-	private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "\n"
-			+ "%s\n" + "";
-
-	private static final String KEY_DESCRIPTOR_TEMPLATE = "\n"
-			+ "\n" + "\n"
-			+ "" + CERTIFICATE + "\n" + "\n" + "\n"
-			+ "";
-
-	private static final String EXTENSIONS_TEMPLATE = "" + "" + "";
-
-	private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = "";
-
-	private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
-
-	private String metadata;
-
-	@BeforeEach
-	public void setup() throws Exception {
-		ClassPathResource resource = new ClassPathResource("test-metadata.xml");
-		try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
-			this.metadata = reader.lines().collect(Collectors.joining());
-		}
-	}
-
-	// gh-12667
-	@Test
-	public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception {
-		try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) {
-			this.converter.convert(source)
-				.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails())
-					.isInstanceOf(OpenSamlAssertingPartyDetails.class));
-		}
-	}
-
-	@Test
-	public void readWhenMissingIDPSSODescriptorThenException() {
-		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, "");
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
-			.withMessageContaining("Metadata response is missing the necessary IDPSSODescriptor element");
-	}
-
-	@Test
-	public void readWhenMissingVerificationKeyThenException() {
-		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, ""));
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
-			.withMessageContaining(
-					"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
-	}
-
-	@Test
-	public void readWhenMissingSingleSignOnServiceThenException() {
-		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
-				String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")));
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
-			.withMessageContaining(
-					"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
-	}
-
-	@Test
-	public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception {
-		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
-				String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
-						String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
-								+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE
-								+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream)
-			.iterator()
-			.next()
-			.build()
-			.getAssertingPartyDetails();
-		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
-		assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512);
-		assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
-		assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
-		assertThat(details.getEntityId()).isEqualTo("entity-id");
-		assertThat(details.getVerificationX509Credentials()).hasSize(1);
-		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
-			.isEqualTo(x509Certificate(CERTIFICATE));
-		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
-	@Test
-	public void readWhenEntitiesDescriptorThenConfigures() throws Exception {
-		String payload = String.format(ENTITIES_DESCRIPTOR_TEMPLATE,
-				String.format(ENTITY_DESCRIPTOR_TEMPLATE,
-						String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
-								String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
-										+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"")
-										+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))));
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream)
-			.iterator()
-			.next()
-			.build()
-			.getAssertingPartyDetails();
-		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
-		assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
-		assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
-		assertThat(details.getEntityId()).isEqualTo("entity-id");
-		assertThat(details.getVerificationX509Credentials()).hasSize(1);
-		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
-			.isEqualTo(x509Certificate(CERTIFICATE));
-		assertThat(details.getEncryptionX509Credentials()).hasSize(1);
-		assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
-			.isEqualTo(x509Certificate(CERTIFICATE));
-	}
-
-	@Test
-	public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception {
-		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.AssertingPartyDetails details = this.converter.convert(inputStream)
-			.iterator()
-			.next()
-			.build()
-			.getAssertingPartyDetails();
-		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
-			.isEqualTo(x509Certificate(CERTIFICATE));
-		assertThat(details.getEncryptionX509Credentials()).hasSize(1);
-		assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
-			.isEqualTo(x509Certificate(CERTIFICATE));
-	}
-
-	X509Certificate x509Certificate(String data) {
-		try {
-			InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes()));
-			return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificate);
-		}
-		catch (Exception ex) {
-			throw new IllegalArgumentException(ex);
-		}
-	}
-
-	// gh-9051
-	@Test
-	public void readWhenUnsupportedElementThenSaml2Exception() {
-		String payload = "";
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
-			.withMessage("Unsupported element of type saml2:Assertion");
-	}
-
-}
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 7d89c998106..9ecbc0bd086 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
@@ -252,6 +252,21 @@ public void collectionFromMetadataInputStreamWhenResolvableThenPopulatesBuilder(
 		}
 	}
 
+	@Test
+	public void fromMetadataLocationWhenResolvableThenUsesEntityIdAndOpenSamlRelyingPartyRegistration()
+			throws Exception {
+		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200));
+			RelyingPartyRegistration registration = RelyingPartyRegistrations
+				.fromMetadataLocation(server.url("/").toString())
+				.entityId("rp")
+				.build();
+			RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
+			assertThat(registration.getRegistrationId()).isEqualTo(details.getEntityId());
+			assertThat(registration).isInstanceOf(OpenSamlRelyingPartyRegistration.class);
+		}
+	}
+
 	@Test
 	public void collectionFromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exception {
 		try (InputStream source = new ByteArrayInputStream("".getBytes())) {
diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java
index c2f547e48b2..e7a4fdab037 100644
--- a/web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java
+++ b/web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java
@@ -18,7 +18,7 @@
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.Scanner;
+import java.util.regex.Pattern;
 
 import jakarta.servlet.http.HttpServletRequest;
 
@@ -37,6 +37,8 @@
  */
 public final class IpAddressMatcher implements RequestMatcher {
 
+	private static Pattern IPV4 = Pattern.compile("\\d{0,3}.\\d{0,3}.\\d{0,3}.\\d{0,3}(/\\d{0,3})?");
+
 	private final int nMaskBits;
 
 	private final InetAddress requiredAddress;
@@ -93,16 +95,13 @@ public boolean matches(String address) {
 	}
 
 	private void assertNotHostName(String ipAddress) {
+		boolean isIpv4 = IPV4.matcher(ipAddress).matches();
+		if (isIpv4) {
+			return;
+		}
 		String error = "ipAddress " + ipAddress + " doesn't look like an IP Address. Is it a host name?";
 		Assert.isTrue(ipAddress.charAt(0) == '[' || ipAddress.charAt(0) == ':'
-				|| Character.digit(ipAddress.charAt(0), 16) != -1, error);
-		if (!ipAddress.contains(":")) {
-			Scanner parts = new Scanner(ipAddress);
-			parts.useDelimiter("[./]");
-			while (parts.hasNext()) {
-				Assert.isTrue(parts.hasNextInt() && parts.nextInt() >> 8 == 0, error);
-			}
-		}
+				|| (Character.digit(ipAddress.charAt(0), 16) != -1 && ipAddress.contains(":")), error);
 	}
 
 	private InetAddress parseAddress(String address) {
diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/ParameterRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/ParameterRequestMatcher.java
new file mode 100644
index 00000000000..a81976de3a6
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/util/matcher/ParameterRequestMatcher.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.util.matcher;
+
+import java.util.Map;
+import java.util.Objects;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * A {@link RequestMatcher} for matching on a request parameter and its value.
+ *
+ * 

+ * The value may also be specified as a placeholder in order to match on any value, + * returning the value as part of the {@link MatchResult}. + * + * @author Josh Cummings + * @since 6.4 + */ +public final class ParameterRequestMatcher implements RequestMatcher { + + private static final MatchesValueMatcher NON_NULL = Objects::nonNull; + + private final String name; + + private final ValueMatcher matcher; + + public ParameterRequestMatcher(String name) { + this.name = name; + this.matcher = NON_NULL; + } + + public ParameterRequestMatcher(String name, String value) { + this.name = name; + MatchesValueMatcher matcher = value::equals; + if (value.startsWith("{") && value.endsWith("}")) { + String key = value.substring(1, value.length() - 1); + this.matcher = (v) -> (v != null) ? MatchResult.match(Map.of(key, v)) : MatchResult.notMatch(); + } + else { + this.matcher = matcher; + } + } + + @Override + public boolean matches(HttpServletRequest request) { + return matcher(request).isMatch(); + } + + @Override + public MatchResult matcher(HttpServletRequest request) { + String parameterValue = request.getParameter(this.name); + return this.matcher.matcher(parameterValue); + } + + private interface ValueMatcher { + + MatchResult matcher(String value); + + } + + private interface MatchesValueMatcher extends ValueMatcher { + + default MatchResult matcher(String value) { + if (matches(value)) { + return MatchResult.match(); + } + else { + return MatchResult.notMatch(); + } + } + + boolean matches(String value); + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java index 2dc36881cb7..753adb789dd 100644 --- a/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java @@ -91,9 +91,7 @@ public void testAccessDeniedWhenAnonymous() throws Exception { request.setContextPath("/mycontext"); request.setRequestURI("/mycontext/secure/page.html"); // Setup the FilterChain to thrown an access denied exception - FilterChain fc = mock(FilterChain.class); - willThrow(new AccessDeniedException("")).given(fc) - .doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(new AccessDeniedException("")); // Setup SecurityContextHolder, as filter needs to check if user is // anonymous SecurityContextHolder.getContext() @@ -119,9 +117,7 @@ public void testAccessDeniedWithRememberMe() throws Exception { request.setContextPath("/mycontext"); request.setRequestURI("/mycontext/secure/page.html"); // Setup the FilterChain to thrown an access denied exception - FilterChain fc = mock(FilterChain.class); - willThrow(new AccessDeniedException("")).given(fc) - .doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(new AccessDeniedException("")); // Setup SecurityContextHolder, as filter needs to check if user is remembered SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication( @@ -142,9 +138,7 @@ public void testAccessDeniedWhenNonAnonymous() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setServletPath("/secure/page.html"); // Setup the FilterChain to thrown an access denied exception - FilterChain fc = mock(FilterChain.class); - willThrow(new AccessDeniedException("")).given(fc) - .doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(new AccessDeniedException("")); // Setup SecurityContextHolder, as filter needs to check if user is // anonymous SecurityContextHolder.clearContext(); @@ -167,9 +161,7 @@ public void testLocalizedErrorMessages() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setServletPath("/secure/page.html"); // Setup the FilterChain to thrown an access denied exception - FilterChain fc = mock(FilterChain.class); - willThrow(new AccessDeniedException("")).given(fc) - .doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(new AccessDeniedException("")); // Setup SecurityContextHolder, as filter needs to check if user is // anonymous SecurityContextHolder.getContext() @@ -198,9 +190,7 @@ public void redirectedToLoginFormAndSessionShowsOriginalTargetWhenAuthentication request.setContextPath("/mycontext"); request.setRequestURI("/mycontext/secure/page.html"); // Setup the FilterChain to thrown an authentication failure exception - FilterChain fc = mock(FilterChain.class); - willThrow(new BadCredentialsException("")).given(fc) - .doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(new BadCredentialsException("")); // Test RequestCache requestCache = new HttpSessionRequestCache(); ExceptionTranslationFilter filter = new ExceptionTranslationFilter(this.mockEntryPoint, requestCache); @@ -223,9 +213,7 @@ public void redirectedToLoginFormAndSessionShowsOriginalTargetWithExoticPortWhen request.setContextPath("/mycontext"); request.setRequestURI("/mycontext/secure/page.html"); // Setup the FilterChain to thrown an authentication failure exception - FilterChain fc = mock(FilterChain.class); - willThrow(new BadCredentialsException("")).given(fc) - .doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(new BadCredentialsException("")); // Test HttpSessionRequestCache requestCache = new HttpSessionRequestCache(); ExceptionTranslationFilter filter = new ExceptionTranslationFilter(this.mockEntryPoint, requestCache); @@ -265,8 +253,7 @@ public void thrownIOExceptionServletExceptionAndRuntimeExceptionsAreRethrown() t filter.afterPropertiesSet(); Exception[] exceptions = { new IOException(), new ServletException(), new RuntimeException() }; for (Exception exception : exceptions) { - FilterChain fc = mock(FilterChain.class); - willThrow(exception).given(fc).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + FilterChain fc = mockFilterChainWiehException(exception); assertThatExceptionOfType(Exception.class) .isThrownBy(() -> filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), fc)) .isSameAs(exception); @@ -304,7 +291,11 @@ public void setMessageSourceWhenNotNullThenCanGet() { filter.messages.getMessage(code); verify(source).getMessage(eq(code), any(), any()); } - + private FilterChain mockFilterChainWiehException(Object exception) throws ServletException, IOException { + FilterChain fc = mock(FilterChain.class); + willThrow((Throwable) exception).given(fc).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + return fc; + } private AuthenticationEntryPoint mockEntryPoint = (request, response, authException) -> response .sendRedirect(request.getContextPath() + "/login.jsp"); diff --git a/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java b/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java index 32e6702fde7..1f72e245fb3 100644 --- a/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java @@ -164,13 +164,8 @@ public void doFilterWhenNoSessionThenChainIsContinued() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); RedirectStrategy redirect = mock(RedirectStrategy.class); - SessionRegistry registry = mock(SessionRegistry.class); - SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); - information.expireNow(); - given(registry.getSessionInformation(anyString())).willReturn(information); String expiredUrl = "/expired"; - ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredUrl); + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(mockSessionRegistry(), expiredUrl); filter.setRedirectStrategy(redirect); MockFilterChain chain = new MockFilterChain(); filter.doFilter(request, response, chain); @@ -199,13 +194,8 @@ public void doFilterWhenCustomRedirectStrategyThenCustomRedirectStrategyUsed() t request.setSession(session); MockHttpServletResponse response = new MockHttpServletResponse(); RedirectStrategy redirect = mock(RedirectStrategy.class); - SessionRegistry registry = mock(SessionRegistry.class); - SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); - information.expireNow(); - given(registry.getSessionInformation(anyString())).willReturn(information); String expiredUrl = "/expired"; - ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredUrl); + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(mockSessionRegistry(), expiredUrl); filter.setRedirectStrategy(redirect); filter.doFilter(request, response, new MockFilterChain()); verify(redirect).sendRedirect(request, response, expiredUrl); @@ -218,13 +208,8 @@ public void doFilterWhenOverrideThenCustomRedirectStrategyUsed() throws Exceptio request.setSession(session); MockHttpServletResponse response = new MockHttpServletResponse(); RedirectStrategy redirect = mock(RedirectStrategy.class); - SessionRegistry registry = mock(SessionRegistry.class); - SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); - information.expireNow(); - given(registry.getSessionInformation(anyString())).willReturn(information); final String expiredUrl = "/expired"; - ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredUrl + "will-be-overrridden") { + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(mockSessionRegistry(), expiredUrl + "will-be-overrridden") { @Override protected String determineExpiredUrl(HttpServletRequest request, SessionInformation info) { return expiredUrl; @@ -241,12 +226,7 @@ public void doFilterWhenNoExpiredUrlThenResponseWritten() throws Exception { MockHttpSession session = new MockHttpSession(); request.setSession(session); MockHttpServletResponse response = new MockHttpServletResponse(); - SessionRegistry registry = mock(SessionRegistry.class); - SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); - information.expireNow(); - given(registry.getSessionInformation(anyString())).willReturn(information); - ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry); + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(mockSessionRegistry()); filter.doFilter(request, response, new MockFilterChain()); assertThat(response.getContentAsString()).contains( "This session has been expired (possibly due to multiple concurrent logins being attempted as the same user)."); @@ -259,12 +239,7 @@ public void doFilterWhenCustomLogoutHandlersThenHandlersUsed() throws Exception MockHttpSession session = new MockHttpSession(); request.setSession(session); MockHttpServletResponse response = new MockHttpServletResponse(); - SessionRegistry registry = mock(SessionRegistry.class); - SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); - information.expireNow(); - given(registry.getSessionInformation(anyString())).willReturn(information); - ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry); + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(mockSessionRegistry()); filter.setLogoutHandlers(new LogoutHandler[] { handler }); filter.doFilter(request, response, new MockFilterChain()); verify(handler).logout(eq(request), eq(response), any()); @@ -276,12 +251,7 @@ public void doFilterWhenCustomSecurityContextHolderStrategyThenHandlersUsed() th MockHttpSession session = new MockHttpSession(); request.setSession(session); MockHttpServletResponse response = new MockHttpServletResponse(); - SessionRegistry registry = mock(SessionRegistry.class); - SessionInformation information = new SessionInformation("user", "sessionId", - new Date(System.currentTimeMillis() - 1000)); - information.expireNow(); - given(registry.getSessionInformation(anyString())).willReturn(information); - ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry); + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(mockSessionRegistry()); SecurityContextHolderStrategy securityContextHolderStrategy = spy( new MockSecurityContextHolderStrategy(new TestingAuthenticationToken("user", "password"))); filter.setSecurityContextHolderStrategy(securityContextHolderStrategy); @@ -300,5 +270,12 @@ public void setLogoutHandlersWhenEmptyThenThrowsException() { ConcurrentSessionFilter filter = new ConcurrentSessionFilter(new SessionRegistryImpl()); assertThatIllegalArgumentException().isThrownBy(() -> filter.setLogoutHandlers(new LogoutHandler[0])); } - + private SessionRegistry mockSessionRegistry(){ + SessionRegistry registry = mock(SessionRegistry.class); + SessionInformation information = new SessionInformation("user", "sessionId", + new Date(System.currentTimeMillis() - 1000)); + information.expireNow(); + given(registry.getSessionInformation(anyString())).willReturn(information); + return registry; + } } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/ParameterRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/ParameterRequestMatcherTests.java new file mode 100644 index 00000000000..6da461ed4ce --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/matcher/ParameterRequestMatcherTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.util.matcher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ParameterRequestMatcher} + * + * @author Josh Cummings + */ +@ExtendWith(MockitoExtension.class) +public class ParameterRequestMatcherTests { + + @Test + public void matchesWhenNameThenMatchesOnParameterName() { + ParameterRequestMatcher matcher = new ParameterRequestMatcher("name"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/bar"); + assertThat(matcher.matches(request)).isFalse(); + request.setParameter("name", "value"); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void matchesWhenNameAndValueThenMatchesOnBoth() { + ParameterRequestMatcher matcher = new ParameterRequestMatcher("name", "value"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/bar"); + request.setParameter("name", "value"); + assertThat(matcher.matches(request)).isTrue(); + request.setParameter("name", "wrong"); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void matchesWhenValuePlaceholderThenMatchesOnName() { + ParameterRequestMatcher matcher = new ParameterRequestMatcher("name", "{placeholder}"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/bar"); + request.setParameter("name", "value"); + RequestMatcher.MatchResult result = matcher.matcher(request); + assertThat(result.isMatch()).isTrue(); + assertThat(result.getVariables().get("placeholder")).isEqualTo("value"); + } + +}