diff --git a/.github/dependabot.template.yml b/.github/dependabot.template.yml index 20976085e84..a4f59248114 100644 --- a/.github/dependabot.template.yml +++ b/.github/dependabot.template.yml @@ -36,6 +36,5 @@ updates: schedule: interval: weekly ignore: - - dependency-name: "sjohnr/*" - dependency-name: "spring-io/*" - dependency-name: "spring-security-release-tools/*" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1fdd60b1779..4a6cc2425df 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,32 +5,7 @@ registries: url: https://repo.spring.io/milestone updates: - package-ecosystem: gradle - target-branch: 5.8.x - directory: / - schedule: - interval: daily - time: '03:00' - timezone: Etc/UTC - labels: - - 'type: dependency-upgrade' - registries: - - spring-milestones - ignore: - - dependency-name: com.nimbusds:nimbus-jose-jwt - - dependency-name: org.python:jython - - dependency-name: org.apache.directory.server:* - - dependency-name: org.junit:junit-bom - update-types: - - version-update:semver-major - - dependency-name: org.mockito:mockito-bom - update-types: - - version-update:semver-major - - dependency-name: '*' - update-types: - - version-update:semver-major - - version-update:semver-minor - - package-ecosystem: gradle - target-branch: 6.2.x + target-branch: 6.3.x directory: / schedule: interval: daily @@ -82,17 +57,15 @@ updates: - dependency-name: '*' update-types: - version-update:semver-major - - version-update:semver-minor + - package-ecosystem: github-actions - target-branch: 5.8.x + target-branch: 6.3.x directory: / schedule: interval: weekly labels: - 'type: task' - 'in: build' - ignore: - - dependency-name: sjohnr/* - package-ecosystem: github-actions target-branch: main directory: / @@ -101,8 +74,6 @@ updates: labels: - 'type: task' - 'in: build' - ignore: - - dependency-name: sjohnr/* - package-ecosystem: github-actions target-branch: docs-build directory: / @@ -111,27 +82,29 @@ updates: labels: - 'type: task' - 'in: build' - ignore: - - dependency-name: sjohnr/* - package-ecosystem: npm target-branch: docs-build directory: / schedule: interval: weekly + labels: + - 'type: task' + - 'in: build' - package-ecosystem: npm target-branch: main directory: /docs schedule: interval: weekly + labels: + - 'type: task' + - 'in: build' - package-ecosystem: npm - target-branch: 6.2.x - directory: /docs - schedule: - interval: weekly - - package-ecosystem: npm - target-branch: 5.8.x + target-branch: 6.3.x directory: /docs schedule: interval: weekly + labels: + - 'type: task' + - 'in: build' diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 1b8d7f12aa1..c092c663890 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -39,7 +39,7 @@ jobs: toolchain: 17 with: java-version: ${{ matrix.java-version }} - test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.1.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2023.1.+ --stacktrace + 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 diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index 99fc0d4a790..2ebf86c76be 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -18,7 +18,7 @@ jobs: java-version: '17' distribution: 'temurin' - name: Build with Gradle - run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue + run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue --scan generate-docs: name: Generate Docs runs-on: ubuntu-latest diff --git a/.github/workflows/release-scheduler.yml b/.github/workflows/release-scheduler.yml index 8b9fdce8948..b3de880e2b5 100644 --- a/.github/workflows/release-scheduler.yml +++ b/.github/workflows/release-scheduler.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: # List of active maintenance branches. - branch: [ main, 6.2.x, 6.1.x, 5.8.x ] + branch: [ main, 6.3.x, 6.2.x, 5.8.x ] runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/update-antora-ui-spring.yml b/.github/workflows/update-antora-ui-spring.yml new file mode 100644 index 00000000000..3863b100571 --- /dev/null +++ b/.github/workflows/update-antora-ui-spring.yml @@ -0,0 +1,35 @@ +name: Update Antora UI Spring + +on: + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: + +permissions: + pull-requests: write + issues: write + contents: write + +jobs: + update-antora-ui-spring: + runs-on: ubuntu-latest + name: Update on Supported Branches + strategy: + matrix: + branch: [ '5.8.x', '6.2.x', '6.3.x', 'main' ] + steps: + - uses: spring-io/spring-doc-actions/update-antora-spring-ui@852920ba3fb1f28b35a2f13201133bc00ef33677 + name: Update + with: + docs-branch: ${{ matrix.branch }} + token: ${{ secrets.GITHUB_TOKEN }} + antora-file-path: 'docs/antora-playbook.yml' + update-antora-ui-spring-docs-build: + runs-on: ubuntu-latest + name: Update on docs-build + steps: + - uses: spring-io/spring-doc-actions/update-antora-spring-ui@852920ba3fb1f28b35a2f13201133bc00ef33677 + name: Update + with: + docs-branch: 'docs-build' + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.adoc b/README.adoc index 73d95c29f40..fdea7d89e26 100644 --- a/README.adoc +++ b/README.adoc @@ -18,9 +18,11 @@ 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]. +You may also want to check out https://docs.spring.io/spring-security/reference/whats-new.html[what's new in the latest release]. + == Quick Start See https://docs.spring.io/spring-security/reference/servlet/getting-started.html[Hello Spring Security] to get started with a "Hello, World" application. diff --git a/acl/src/main/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImpl.java b/acl/src/main/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImpl.java index de50e7563a9..07349531b43 100644 --- a/acl/src/main/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImpl.java +++ b/acl/src/main/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImpl.java @@ -17,10 +17,13 @@ package org.springframework.security.acls.domain; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Set; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.acls.model.Acl; import org.springframework.security.acls.model.Sid; import org.springframework.security.acls.model.SidRetrievalStrategy; @@ -59,6 +62,8 @@ public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy { private SidRetrievalStrategy sidRetrievalStrategy = new SidRetrievalStrategyImpl(); + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + /** * Constructor. The only mandatory parameter relates to the system-wide * {@link GrantedAuthority} instances that can be held to always permit ACL changes. @@ -100,7 +105,9 @@ public void securityCheck(Acl acl, int changeType) { } // Iterate this principal's authorities to determine right - Set authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); + Collection reachableGrantedAuthorities = this.roleHierarchy + .getReachableGrantedAuthorities(authentication.getAuthorities()); + Set authorities = AuthorityUtils.authorityListToSet(reachableGrantedAuthorities); if (acl.getOwner() instanceof GrantedAuthoritySid && authorities.contains(((GrantedAuthoritySid) acl.getOwner()).getGrantedAuthority())) { return; @@ -162,4 +169,14 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur this.securityContextHolderStrategy = securityContextHolderStrategy; } + /** + * Sets the {@link RoleHierarchy} to use. The default is to use a + * {@link NullRoleHierarchy} + * @since 6.4 + */ + public void setRoleHierarchy(RoleHierarchy roleHierarchy) { + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; + } + } diff --git a/acl/src/main/java/org/springframework/security/acls/domain/AclImpl.java b/acl/src/main/java/org/springframework/security/acls/domain/AclImpl.java index 5f7a0532a36..8550450e8f2 100644 --- a/acl/src/main/java/org/springframework/security/acls/domain/AclImpl.java +++ b/acl/src/main/java/org/springframework/security/acls/domain/AclImpl.java @@ -202,7 +202,7 @@ public boolean isGranted(List permission, List sids, boolean ad public boolean isSidLoaded(List sids) { // If loadedSides is null, this indicates all SIDs were loaded // Also return true if the caller didn't specify a SID to find - if ((this.loadedSids == null) || (sids == null) || (sids.size() == 0)) { + if ((this.loadedSids == null) || (sids == null) || sids.isEmpty()) { return true; } diff --git a/acl/src/main/java/org/springframework/security/acls/domain/DefaultPermissionFactory.java b/acl/src/main/java/org/springframework/security/acls/domain/DefaultPermissionFactory.java index 8a68843d89f..81cbe8a5735 100644 --- a/acl/src/main/java/org/springframework/security/acls/domain/DefaultPermissionFactory.java +++ b/acl/src/main/java/org/springframework/security/acls/domain/DefaultPermissionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -140,7 +140,7 @@ public Permission buildFromName(String name) { @Override public List buildFromNames(List names) { - if ((names == null) || (names.size() == 0)) { + if ((names == null) || names.isEmpty()) { return Collections.emptyList(); } List permissions = new ArrayList<>(names.size()); diff --git a/acl/src/test/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImplTests.java b/acl/src/test/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImplTests.java index 9a61e6d2502..531e18b0b4e 100644 --- a/acl/src/test/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImplTests.java +++ b/acl/src/test/java/org/springframework/security/acls/domain/AclAuthorizationStrategyImplTests.java @@ -25,6 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.acls.model.Acl; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -34,6 +35,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextImpl; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -86,6 +88,15 @@ public void securityCheckWhenAclOwnedByGrantedAuthority() { this.strategy.securityCheck(this.acl, AclAuthorizationStrategy.CHANGE_GENERAL); } + @Test + public void securityCheckWhenRoleReachableByHierarchyThenAuthorized() { + given(this.acl.getOwner()).willReturn(new GrantedAuthoritySid("ROLE_AUTH_B")); + this.strategy = new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_SYSTEM_ADMIN")); + this.strategy.setRoleHierarchy(RoleHierarchyImpl.fromHierarchy("ROLE_AUTH > ROLE_AUTH_B")); + assertThatNoException() + .isThrownBy(() -> this.strategy.securityCheck(this.acl, AclAuthorizationStrategy.CHANGE_GENERAL)); + } + @Test public void securityCheckWhenCustomSecurityContextHolderStrategyThenUses() { given(this.securityContextHolderStrategy.getContext()).willReturn(this.context); diff --git a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java index f80ad94d633..d978b3c8ea0 100644 --- a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java +++ b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java @@ -47,6 +47,8 @@ public class PreAuthorizeAspectTests { private PrePostSecured prePostSecured = new PrePostSecured(); + private MultipleInterfaces multiple = new MultipleInterfaces(); + @BeforeEach public final void setUp() { MockitoAnnotations.initMocks(this); @@ -110,6 +112,12 @@ public void nestedDenyAllPreAuthorizeDeniesAccess() { .isThrownBy(() -> this.secured.myObject().denyAllMethod()); } + @Test + public void multipleInterfacesPreAuthorizeAllows() { + // aspectj doesn't inherit annotations + this.multiple.securedMethod(); + } + interface SecuredInterface { @PreAuthorize("hasRole('X')") @@ -177,4 +185,19 @@ void denyAllMethod() { } + interface AnotherSecuredInterface { + + @PreAuthorize("hasRole('Y')") + void securedMethod(); + + } + + static class MultipleInterfaces implements SecuredInterface, AnotherSecuredInterface { + + @Override + public void securedMethod() { + } + + } + } diff --git a/build.gradle b/build.gradle index e3dc49f7fc5..25f41aa7800 100644 --- a/build.gradle +++ b/build.gradle @@ -105,10 +105,14 @@ develocity { } nohttp { - source.exclude "buildSrc/build/**" + source.exclude "buildSrc/build/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**" source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd)) } +tasks.named('checkstyleNohttp') { + maxHeapSize = '1g' +} + tasks.register('cloneRepository', IncludeRepoTask) { repository = project.getProperties().get("repositoryName") ref = project.getProperties().get("ref") @@ -120,7 +124,7 @@ wrapperUpgrade { gradle { 'spring-security' { repo = 'spring-projects/spring-security' - baseBranch = '6.1.x' // runs only on 6.1.x and the update is merged forward to main + baseBranch = '6.2.x' // runs only on 6.2.x and the update is merged forward to main } } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/maven/PublishAllJavaComponentsPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/maven/PublishAllJavaComponentsPlugin.java index 408d83e716f..d89836048ce 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/maven/PublishAllJavaComponentsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/maven/PublishAllJavaComponentsPlugin.java @@ -7,8 +7,6 @@ import org.gradle.api.plugins.JavaPlatformPlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.publish.PublishingExtension; -import org.gradle.api.publish.VariantVersionMappingStrategy; -import org.gradle.api.publish.VersionMappingStrategy; import org.gradle.api.publish.maven.MavenPublication; import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java index d10a06673fa..fab23a49557 100644 --- a/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.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. @@ -23,7 +23,6 @@ import org.gradle.api.artifacts.MinimalExternalModuleDependency; import org.gradle.api.artifacts.VersionCatalog; import org.gradle.api.artifacts.VersionCatalogsExtension; -import org.gradle.api.file.RegularFile; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.provider.Property; @@ -36,7 +35,6 @@ import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.VerificationException; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Optional; diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java index 5b390fe19be..0779a509a6a 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java @@ -56,6 +56,7 @@ * * @author Ben Alex * @author Scott Battaglia + * @author Kim Youngwoong */ public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { @@ -63,7 +64,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia private AuthenticationUserDetailsService authenticationUserDetailsService; - private final UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker(); + private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); @@ -187,6 +188,17 @@ public void setAuthenticationUserDetailsService( this.authenticationUserDetailsService = authenticationUserDetailsService; } + /** + * Sets the UserDetailsChecker to be used for checking the status of retrieved user + * details. This allows customization of the UserDetailsChecker implementation. + * @param userDetailsChecker the UserDetailsChecker to be set + * @since 6.4 + */ + public void setUserDetailsChecker(final UserDetailsChecker userDetailsChecker) { + Assert.notNull(userDetailsChecker, "userDetailsChecker cannot be null"); + this.userDetailsChecker = userDetailsChecker; + } + public void setServiceProperties(final ServiceProperties serviceProperties) { this.serviceProperties = serviceProperties; } diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java index 7efdf8e9395..0bb9c4d28bc 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java @@ -115,15 +115,8 @@ public boolean equals(final Object obj) { if (!super.equals(obj)) { return false; } - if (obj instanceof CasAuthenticationToken) { - CasAuthenticationToken test = (CasAuthenticationToken) obj; - if (!this.assertion.equals(test.getAssertion())) { - return false; - } - if (this.getKeyHash() != test.getKeyHash()) { - return false; - } - return true; + if (obj instanceof CasAuthenticationToken test) { + return this.assertion.equals(test.getAssertion()) && this.getKeyHash() == test.getKeyHash(); } return false; } diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java b/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java index b6c7c6f8fad..fad74fdb7b6 100644 --- a/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java +++ b/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java @@ -41,6 +41,7 @@ * @since 4.2 * @see org.springframework.security.jackson2.SecurityJackson2Modules */ +@SuppressWarnings("serial") public class CasJackson2Module extends SimpleModule { public CasJackson2Module() { diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java index f11f1915073..0e8b6fe8627 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.apereo.cas.client.validation.Assertion; import org.apereo.cas.client.validation.AssertionImpl; @@ -31,11 +32,13 @@ import org.springframework.security.cas.ServiceProperties; import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsChecker; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.WebAuthenticationDetails; @@ -55,6 +58,7 @@ * * @author Ben Alex * @author Scott Battaglia + * @author Kim Youngwoong */ @SuppressWarnings("unchecked") public class CasAuthenticationProviderTests { @@ -320,6 +324,29 @@ public void supportsRequiredTokens() { assertThat(cap.supports(CasAuthenticationToken.class)).isTrue(); } + @Test + public void testSetUserDetailsChecker() throws AuthenticationException { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); + cap.setKey("qwerty"); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.setServiceProperties(makeServiceProperties()); + cap.afterPropertiesSet(); + CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123"); + + AtomicInteger checkCount = new AtomicInteger(0); + UserDetailsChecker userDetailsChecker = new UserDetailsChecker() { + @Override + public void check(UserDetails user) { + checkCount.incrementAndGet(); + } + }; + cap.setUserDetailsChecker(userDetailsChecker); + cap.authenticate(token); + + assertThat(checkCount.get()).isEqualTo(1); + } + private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService { @Override diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 746f51ee382..90e7b883129 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -6,6 +6,12 @@ apply plugin: 'io.spring.convention.spring-module' apply plugin: 'trang' apply plugin: 'kotlin' +configurations { + opensaml5 { + extendsFrom(optional, tests) + } +} + dependencies { management platform(project(":spring-security-dependencies")) // NB: Don't add other compile time dependencies to the config module as this breaks tooling @@ -15,9 +21,11 @@ dependencies { api 'org.springframework:spring-context' api 'org.springframework:spring-core' + optional project(':spring-security-data') optional project(':spring-security-ldap') optional project(':spring-security-messaging') - optional project(':spring-security-saml2-service-provider') + optional project(path: ':spring-security-saml2-service-provider') + opensaml5 project(path: ':spring-security-saml2-service-provider', configuration: 'opensamlFiveMain') optional project(':spring-security-oauth2-client') optional project(':spring-security-oauth2-jose') optional project(':spring-security-oauth2-resource-server') @@ -35,6 +43,7 @@ dependencies { optional 'org.jetbrains.kotlin:kotlin-reflect' optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' optional 'jakarta.annotation:jakarta.annotation-api' + optional libs.webauthn4j.core provided 'jakarta.servlet:jakarta.servlet-api' @@ -65,7 +74,7 @@ dependencies { testImplementation 'jakarta.websocket:jakarta.websocket-api' testImplementation 'jakarta.websocket:jakarta.websocket-client-api' testImplementation 'ldapsdk:ldapsdk:4.1' - testImplementation('net.sourceforge.htmlunit:htmlunit') { + testImplementation('org.htmlunit:htmlunit') { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'xml-apis', module: 'xml-apis' } @@ -80,7 +89,7 @@ dependencies { testImplementation "org.hibernate.orm:hibernate-core" testImplementation 'org.hsqldb:hsqldb' testImplementation 'org.mockito:mockito-core' - testImplementation('org.seleniumhq.selenium:htmlunit-driver') { + testImplementation('org.seleniumhq.selenium:htmlunit3-driver') { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'xml-apis', module: 'xml-apis' } @@ -113,6 +122,8 @@ dependencies { exclude group: "org.slf4j", module: "jcl-over-slf4j" } testImplementation libs.org.instancio.instancio.junit + testImplementation libs.org.eclipse.jetty.jetty.server + testImplementation libs.org.eclipse.jetty.jetty.servlet testRuntimeOnly 'org.hsqldb:hsqldb' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -161,3 +172,26 @@ configure(project.tasks.withType(Test)) { systemProperties['springSecurityVersion'] = version } } + +test { + onOutput { descriptor, event -> + if (!project.hasProperty('serialization')) { + return + } + if (descriptor.name=='listClassesMissingSerialVersion()') { + logger.lifecycle(event.message) + } + } +} + +tasks.register("opensaml5Test", Test) { + filter { + includeTestsMatching "org.springframework.security.config.annotation.web.configurers.saml2.*" + } + useJUnitPlatform() + classpath = sourceSets.main.output + sourceSets.test.output + configurations.opensaml5 +} + +tasks.named("check") { + dependsOn opensaml5Test +} diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/configurers/WebAuthnWebDriverTests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/configurers/WebAuthnWebDriverTests.java new file mode 100644 index 00000000000..cc5d7a3501f --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/configurers/WebAuthnWebDriverTests.java @@ -0,0 +1,356 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.configurers; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AbstractStringAssert; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriverService; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.chromium.HasCdp; +import org.openqa.selenium.devtools.HasDevTools; +import org.openqa.selenium.remote.Augmenter; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.support.ui.FluentWait; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Webdriver-based tests for the WebAuthnConfigurer. This uses a full browser because + * these features require Javascript and browser APIs to be available. + * + * @author Daniel Garnier-Moiroux + */ +class WebAuthnWebDriverTests { + + private String baseUrl; + + private static ChromeDriverService driverService; + + private Server server; + + private RemoteWebDriver driver; + + private static final String USERNAME = "user"; + + private static final String PASSWORD = "password"; + + @BeforeAll + static void startChromeDriverService() throws Exception { + driverService = new ChromeDriverService.Builder().usingAnyFreePort().build(); + driverService.start(); + } + + @AfterAll + static void stopChromeDriverService() { + driverService.stop(); + } + + @BeforeEach + void startServer() throws Exception { + // Create the server on port 8080 + this.server = new Server(0); + + // Set up the ServletContextHandler + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + this.server.setHandler(contextHandler); + this.server.start(); + int serverPort = ((ServerConnector) this.server.getConnectors()[0]).getLocalPort(); + this.baseUrl = "http://localhost:" + serverPort; + + // Set up Spring application context + AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); + applicationContext.register(WebAuthnConfiguration.class); + applicationContext.setServletContext(contextHandler.getServletContext()); + + // Add the server port + MockPropertySource propertySource = new MockPropertySource().withProperty("server.port", serverPort); + applicationContext.getEnvironment().getPropertySources().addFirst(propertySource); + + // Register the filter chain + DelegatingFilterProxy filterProxy = new DelegatingFilterProxy("securityFilterChain", applicationContext); + FilterHolder filterHolder = new FilterHolder(filterProxy); + contextHandler.addFilter(filterHolder, "/*", null); + } + + @AfterEach + void stopServer() throws Exception { + this.server.stop(); + } + + @BeforeEach + void setupDriver() { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless=new"); + RemoteWebDriver baseDriver = new RemoteWebDriver(driverService.getUrl(), options); + // Enable dev tools + this.driver = (RemoteWebDriver) new Augmenter().augment(baseDriver); + this.driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(1)); + } + + @AfterEach + void cleanupDriver() { + this.driver.quit(); + } + + @Test + void loginWhenNoValidAuthenticatorCredentialsThenRejects() { + createVirtualAuthenticator(true); + this.driver.get(this.baseUrl); + this.driver.findElement(signinWithPasskeyButton()).click(); + await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?error")); + } + + @Test + void registerWhenNoLabelThenRejects() { + login(); + + this.driver.get(this.baseUrl + "/webauthn/register"); + + this.driver.findElement(registerPasskeyButton()).click(); + assertHasAlertStartingWith("error", "Error: Passkey Label is required"); + } + + @Test + void registerWhenAuthenticatorNoUserVerificationThenRejects() { + createVirtualAuthenticator(false); + login(); + this.driver.get(this.baseUrl + "/webauthn/register"); + this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator"); + this.driver.findElement(registerPasskeyButton()).click(); + + await(() -> assertHasAlertStartingWith("error", + "Registration failed. Call to navigator.credentials.create failed:")); + } + + /** + * Test in 4 steps to verify the end-to-end flow of registering an authenticator and + * using it to register. + *
    + *
  • Step 1: Log in with username / password
  • + *
  • Step 2: Register a credential from the virtual authenticator
  • + *
  • Step 3: Log out
  • + *
  • Step 4: Log in with the authenticator
  • + *
+ */ + @Test + void loginWhenAuthenticatorRegisteredThenSuccess() { + // Setup + createVirtualAuthenticator(true); + + // Step 1: log in with username / password + login(); + + // Step 2: register a credential from the virtual authenticator + this.driver.get(this.baseUrl + "/webauthn/register"); + this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator"); + this.driver.findElement(registerPasskeyButton()).click(); + + // Ensure the page location has changed before performing further assertions. + // This is required because the location change is asynchronously performed in + // javascript, and performing assertions based on this.driver.findElement(...) + // may result in a StaleElementReferenceException. + await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?success")); + await(() -> assertHasAlertStartingWith("success", "Success!")); + + List passkeyRows = this.driver.findElements(passkeyTableRows()); + assertThat(passkeyRows).hasSize(1) + .first() + .extracting((row) -> row.findElement(firstCell())) + .extracting(WebElement::getText) + .isEqualTo("Virtual authenticator"); + + // Step 3: log out + logout(); + + // Step 4: log in with the virtual authenticator + this.driver.get(this.baseUrl + "/webauthn/register"); + this.driver.findElement(signinWithPasskeyButton()).click(); + await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?continue")); + } + + /** + * Add a virtual authenticator. + *

+ * Note that Selenium docs for {@link HasCdp} strongly encourage to use + * {@link HasDevTools} instead. However, devtools require more dependencies and + * boilerplate, notably to sync the Devtools-CDP version with the current browser + * version, whereas CDP runs out of the box. + *

+ * @param userIsVerified whether the authenticator simulates user verification. + * Setting it to false will make the ceremonies fail. + * @see https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/ + */ + private void createVirtualAuthenticator(boolean userIsVerified) { + HasCdp cdpDriver = (HasCdp) this.driver; + cdpDriver.executeCdpCommand("WebAuthn.enable", Map.of("enableUI", false)); + // this.driver.addVirtualAuthenticator(createVirtualAuthenticatorOptions()); + //@formatter:off + cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator", + Map.of( + "options", + Map.of( + "protocol", "ctap2", + "transport", "usb", + "hasUserVerification", true, + "hasResidentKey", true, + "isUserVerified", userIsVerified, + "automaticPresenceSimulation", true + ) + )); + //@formatter:on + } + + private void login() { + this.driver.get(this.baseUrl); + this.driver.findElement(usernameField()).sendKeys(USERNAME); + this.driver.findElement(passwordField()).sendKeys(PASSWORD); + this.driver.findElement(signinWithUsernamePasswordButton()).click(); + } + + private void logout() { + this.driver.get(this.baseUrl + "/logout"); + this.driver.findElement(logoutButton()).click(); + await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?logout")); + } + + private AbstractStringAssert assertHasAlertStartingWith(String alertType, String alertMessage) { + WebElement alert = this.driver.findElement(new By.ById(alertType)); + assertThat(alert.isDisplayed()) + .withFailMessage( + () -> alertType + " alert was not displayed. Full page source:\n\n" + this.driver.getPageSource()) + .isTrue(); + + return assertThat(alert.getText()).startsWith(alertMessage); + } + + /** + * Await until the assertion passes. If the assertion fails, it will display the + * assertion error in stdout. WebDriver-related exceptions are ignored, so that + * {@code assertion}s can interact with the page and be retried on error, e.g. + * {@code assertThat(this.driver.findElement(By.Id("some-id")).isNotNull()}. + */ + private void await(Supplier> assertion) { + new FluentWait<>(this.driver).withTimeout(Duration.ofSeconds(2)) + .pollingEvery(Duration.ofMillis(100)) + .ignoring(AssertionError.class, WebDriverException.class) + .until((d) -> { + assertion.get(); + return true; + }); + } + + private static By.ById passkeyLabel() { + return new By.ById("label"); + } + + private static By.ById registerPasskeyButton() { + return new By.ById("register"); + } + + private static By.ByCssSelector passkeyTableRows() { + return new By.ByCssSelector("table > tbody > tr"); + } + + private static By.ByCssSelector firstCell() { + return new By.ByCssSelector("td:first-child"); + } + + private static By.ById passwordField() { + return new By.ById(PASSWORD); + } + + private static By.ById usernameField() { + return new By.ById("username"); + } + + private static By.ByCssSelector signinWithUsernamePasswordButton() { + return new By.ByCssSelector("form > button[type=\"submit\"]"); + } + + private static By.ById signinWithPasskeyButton() { + return new By.ById("passkey-signin"); + } + + private static By.ByCssSelector logoutButton() { + return new By.ByCssSelector("button"); + } + + /** + * The configuration for WebAuthN tests. It accesses the Server's current port, so we + * can configurer WebAuthnConfigurer#allowedOrigin + */ + @Configuration + @EnableWebMvc + @EnableWebSecurity + static class WebAuthnConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder().username(USERNAME).password(PASSWORD).build()); + } + + @Bean + FilterChainProxy securityFilterChain(HttpSecurity http, Environment environment) throws Exception { + SecurityFilterChain securityFilterChain = http + .authorizeHttpRequests((auth) -> auth.anyRequest().authenticated()) + .formLogin(Customizer.withDefaults()) + .webAuthn((passkeys) -> passkeys.rpId("localhost") + .rpName("Spring Security WebAuthN tests") + .allowedOrigins("http://localhost:" + environment.getProperty("server.port"))) + .build(); + return new FilterChainProxy(securityFilterChain); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketObservationITests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketObservationITests.java new file mode 100644 index 00000000000..9b6cf17b8ce --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketObservationITests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.rsocket; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; +import org.springframework.security.rsocket.metadata.SimpleAuthenticationEncoder; +import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Rob Winch + */ +@ContextConfiguration +@ExtendWith(SpringExtension.class) +public class HelloRSocketObservationITests { + + @Autowired + RSocketMessageHandler handler; + + @Autowired + SecuritySocketAcceptorInterceptor interceptor; + + @Autowired + ServerController controller; + + @Autowired + ObservationHandler observationHandler; + + private CloseableChannel server; + + private RSocketRequester requester; + + @BeforeEach + public void setup() { + // @formatter:off + this.server = RSocketServer.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .interceptors((registry) -> + registry.forSocketAcceptor(this.interceptor) + ) + .acceptor(this.handler.responder()) + .bind(TcpServerTransport.create("localhost", 0)) + .block(); + // @formatter:on + } + + @AfterEach + public void dispose() { + this.requester.rsocket().dispose(); + this.server.dispose(); + this.controller.payloads.clear(); + } + + @Test + public void getWhenUsingObservationRegistryThenObservesRequest() { + UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("rob", "password"); + // @formatter:off + this.requester = RSocketRequester.builder() + .setupMetadata(credentials, MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString())) + .rsocketStrategies(this.handler.getRSocketStrategies()) + .connectTcp("localhost", this.server.address().getPort()) + .block(); + // @formatter:on + String data = "rob"; + // @formatter:off + this.requester.route("secure.retrieve-mono") + .metadata(credentials, MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString())) + .data(data) + .retrieveMono(String.class) + .block(); + // @formatter:on + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(this.observationHandler, times(2)).onStart(captor.capture()); + Iterator contexts = captor.getAllValues().iterator(); + // once for setup + assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications"); + // once for request + assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications"); + } + + @Configuration + @EnableRSocketSecurity + static class Config { + + private ObservationHandler handler = mock(ObservationHandler.class); + + @Bean + ServerController controller() { + return new ServerController(); + } + + @Bean + RSocketMessageHandler messageHandler() { + RSocketMessageHandler handler = new RSocketMessageHandler(); + handler.setRSocketStrategies(rsocketStrategies()); + return handler; + } + + @Bean + RSocketStrategies rsocketStrategies() { + return RSocketStrategies.builder().encoder(new SimpleAuthenticationEncoder()).build(); + } + + @Bean + MapReactiveUserDetailsService uds() { + // @formatter:off + UserDetails rob = User.withDefaultPasswordEncoder() + .username("rob") + .password("password") + .roles("USER", "ADMIN") + .build(); + // @formatter:on + return new MapReactiveUserDetailsService(rob); + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistry observationRegistry() { + given(this.handler.supportsContext(any())).willReturn(true); + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(this.handler); + return registry; + } + + } + + @Controller + static class ServerController { + + private List payloads = new ArrayList<>(); + + @MessageMapping("**") + String retrieveMono(String payload) { + add(payload); + return "Hi " + payload; + } + + private void add(String p) { + this.payloads.add(p); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketWithWebFluxITests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketWithWebFluxITests.java new file mode 100644 index 00000000000..7b298b3c75c --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketWithWebFluxITests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.rsocket; + +import java.util.ArrayList; +import java.util.List; + +import io.rsocket.core.RSocketServer; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; +import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rob Winch + */ +@ContextConfiguration +@ExtendWith(SpringExtension.class) +public class HelloRSocketWithWebFluxITests { + + @Autowired + RSocketMessageHandler handler; + + @Autowired + SecuritySocketAcceptorInterceptor interceptor; + + @Autowired + ServerController controller; + + private CloseableChannel server; + + private RSocketRequester requester; + + @BeforeEach + public void setup() { + // @formatter:off + this.server = RSocketServer.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .interceptors((registry) -> + registry.forSocketAcceptor(this.interceptor) + ) + .acceptor(this.handler.responder()) + .bind(TcpServerTransport.create("localhost", 0)) + .block(); + // @formatter:on + } + + @AfterEach + public void dispose() { + this.requester.rsocket().dispose(); + this.server.dispose(); + this.controller.payloads.clear(); + } + + // gh-16161 + @Test + public void retrieveMonoWhenSecureThenDenied() { + // @formatter:off + this.requester = RSocketRequester.builder() + .rsocketStrategies(this.handler.getRSocketStrategies()) + .connectTcp("localhost", this.server.address().getPort()) + .block(); + // @formatter:on + String data = "rob"; + // @formatter:off + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> this.requester.route("secure.retrieve-mono") + .data(data) + .retrieveMono(String.class) + .block() + ) + .matches((ex) -> ex instanceof RejectedSetupException + || ex.getClass().toString().contains("ReactiveException")); + // @formatter:on + assertThat(this.controller.payloads).isEmpty(); + } + + @Configuration + @EnableRSocketSecurity + @EnableWebFluxSecurity + static class Config { + + @Bean + ServerController controller() { + return new ServerController(); + } + + @Bean + RSocketMessageHandler messageHandler() { + RSocketMessageHandler handler = new RSocketMessageHandler(); + handler.setRSocketStrategies(rsocketStrategies()); + return handler; + } + + @Bean + RSocketStrategies rsocketStrategies() { + return RSocketStrategies.builder().encoder(new BasicAuthenticationEncoder()).build(); + } + + @Bean + MapReactiveUserDetailsService uds() { + // @formatter:off + UserDetails rob = User.withDefaultPasswordEncoder() + .username("rob") + .password("password") + .roles("USER", "ADMIN") + .build(); + // @formatter:on + return new MapReactiveUserDetailsService(rob); + } + + } + + @Controller + static class ServerController { + + private List payloads = new ArrayList<>(); + + @MessageMapping("**") + String retrieveMono(String payload) { + add(payload); + return "Hi " + payload; + } + + private void add(String p) { + this.payloads.add(p); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/ObjectPostProcessor.java new file mode 100644 index 00000000000..69b5ca357f5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ObjectPostProcessor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config; + +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * Allows initialization of Objects. Typically this is used to call the {@link Aware} + * methods, {@link InitializingBean#afterPropertiesSet()}, and ensure that + * {@link DisposableBean#destroy()} has been invoked. + * + * @param the bound of the types of Objects this {@link ObjectPostProcessor} supports. + * @author Rob Winch + * @since 3.2 + */ +public interface ObjectPostProcessor { + + static ObjectPostProcessor identity() { + return new ObjectPostProcessor<>() { + @Override + public O postProcess(O object) { + return object; + } + }; + } + + /** + * Initialize the object possibly returning a modified instance that should be used + * instead. + * @param object the object to initialize + * @return the initialized version of the object + */ + O postProcess(O object); + +} diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index 260b46a4cd7..3ad4b92c614 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-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. @@ -96,7 +96,7 @@ public BeanDefinition parse(Element element, ParserContext pc) { pc.getReaderContext() .fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or " + "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema " - + "with Spring Security 6.3. Please update your schema declarations to the 6.3 schema.", + + "with Spring Security 6.4. Please update your schema declarations to the 6.4 schema.", element); } String name = pc.getDelegate().getLocalName(element); @@ -221,7 +221,7 @@ private boolean namespaceMatchesVersion(Element element) { private boolean matchesVersionInternal(Element element) { String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"); - return schemaLocation.matches("(?m).*spring-security-6\\.3.*.xsd.*") + return schemaLocation.matches("(?m).*spring-security-6\\.4.*.xsd.*") || schemaLocation.matches("(?m).*spring-security.xsd.*") || !schemaLocation.matches("(?m).*spring-security.*"); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java index 42939264605..f89e06ca796 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.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. @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.util.Assert; import org.springframework.web.filter.DelegatingFilterProxy; @@ -78,6 +79,15 @@ protected AbstractConfiguredSecurityBuilder(ObjectPostProcessor objectPo this(objectPostProcessor, false); } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + protected AbstractConfiguredSecurityBuilder( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + this(objectPostProcessor, false); + } + /*** * Creates a new instance with the provided {@link ObjectPostProcessor}. This post * processor must support Object since there are many types of objects that may be @@ -93,6 +103,18 @@ protected AbstractConfiguredSecurityBuilder(ObjectPostProcessor objectPo this.allowConfigurersOfSameType = allowConfigurersOfSameType; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + protected AbstractConfiguredSecurityBuilder( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor, + boolean allowConfigurersOfSameType) { + Assert.notNull(objectPostProcessor, "objectPostProcessor cannot be null"); + this.objectPostProcessor = objectPostProcessor; + this.allowConfigurersOfSameType = allowConfigurersOfSameType; + } + /** * Similar to {@link #build()} and {@link #getObject()} but checks the state to * determine if {@link #build()} needs to be called first. diff --git a/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java index 53a43d1ce98..9d63541438e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java +++ b/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java @@ -28,8 +28,11 @@ * @param the bound of the types of Objects this {@link ObjectPostProcessor} supports. * @author Rob Winch * @since 3.2 + * @deprecated please use {@link org.springframework.security.config.ObjectPostProcessor} + * instead */ -public interface ObjectPostProcessor { +@Deprecated +public interface ObjectPostProcessor extends org.springframework.security.config.ObjectPostProcessor { /** * Initialize the object possibly returning a modified instance that should be used diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java index 7703c974bd5..8136bf70703 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.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. @@ -21,6 +21,7 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.util.Assert; /** @@ -91,6 +92,15 @@ public void addObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { this.objectPostProcessor.addObjectPostProcessor(objectPostProcessor); } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public void addObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + this.objectPostProcessor.addObjectPostProcessor(objectPostProcessor); + } + /** * Sets the {@link SecurityBuilder} to be used. This is automatically set when using * {@link AbstractConfiguredSecurityBuilder#apply(SecurityConfigurerAdapter)} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java index 73e8a31a449..1368d3c823b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.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. @@ -26,8 +26,8 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; @@ -67,12 +67,23 @@ public class AuthenticationManagerBuilder /** * Creates a new instance - * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use. + * @param objectPostProcessor the + * {@link org.springframework.security.config.annotation.ObjectPostProcessor} instance + * to use. */ public AuthenticationManagerBuilder(ObjectPostProcessor objectPostProcessor) { super(objectPostProcessor, true); } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public AuthenticationManagerBuilder( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor, true); + } + /** * Allows providing a parent {@link AuthenticationManager} that will be tried if this * {@link AuthenticationManager} was unable to attempt to authenticate the provided diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index e85fdb0886a..1ebd7b8f7a2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.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. @@ -28,7 +28,6 @@ import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -40,7 +39,7 @@ import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; @@ -57,6 +56,7 @@ * Exports the authentication {@link Configuration} * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 * */ @@ -197,15 +197,6 @@ private AuthenticationManager getAuthenticationManagerBean() { return lazyBean(AuthenticationManager.class); } - private static T getBeanOrNull(ApplicationContext applicationContext, Class type) { - try { - return applicationContext.getBean(type); - } - catch (NoSuchBeanDefinitionException notFound) { - return null; - } - } - private static class EnableGlobalAuthenticationAutowiredConfigurer extends GlobalAuthenticationConfigurerAdapter { private final ApplicationContext context; @@ -330,12 +321,9 @@ private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; } - PasswordEncoder passwordEncoder = getBeanOrNull(this.applicationContext, PasswordEncoder.class); - if (passwordEncoder == null) { - passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - } - this.passwordEncoder = passwordEncoder; - return passwordEncoder; + this.passwordEncoder = this.applicationContext.getBeanProvider(PasswordEncoder.class) + .getIfUnique(PasswordEncoderFactories::createDelegatingPasswordEncoder); + return this.passwordEncoder; } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index c2a47b76108..7320840d9b3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -38,6 +38,7 @@ * {@link PasswordEncoder} is defined will wire this up too. * * @author Rob Winch + * @author Ngoc Nhan * @since 4.1 */ @Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER) @@ -70,10 +71,11 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception { if (auth.isConfigured()) { if (beanNames.length > 0) { this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " - + "UserDetailsService beans will not be used for username/password login. " + + "UserDetailsService beans will not be used by Spring Security for automatically configuring username/password login. " + "Consider removing the AuthenticationProvider bean. " - + "Alternatively, consider using the UserDetailsService in a manually instantiated " - + "DaoAuthenticationProvider."); + + "Alternatively, consider using the UserDetailsService in a manually instantiated DaoAuthenticationProvider. " + + "If the current configuration is intentional, to turn off this warning, " + + "increase the logging level of 'org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer' to ERROR"); } return; } @@ -118,11 +120,7 @@ else if (beanNames.length > 1) { * component, null otherwise. */ private T getBeanOrNull(Class type) { - String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type); - if (beanNames.length != 1) { - return null; - } - return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type); + return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java index 45fd3208c94..7bfc1e91000 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.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. @@ -22,7 +22,7 @@ import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; @@ -140,6 +140,16 @@ public LdapAuthenticationProviderConfigurer withObjectPostProcessor(ObjectPos return this; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public LdapAuthenticationProviderConfigurer withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return this; + } + /** * Gets the {@link LdapAuthoritiesPopulator} and defaults to * {@link DefaultLdapAuthoritiesPopulator} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java index 6acd120958b..519c2bd53f1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -17,7 +17,7 @@ package org.springframework.security.config.annotation.authentication.configurers.userdetails; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; import org.springframework.security.core.userdetails.UserDetailsPasswordService; @@ -63,6 +63,17 @@ public C withObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { return (C) this; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + @SuppressWarnings("unchecked") + public C withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return (C) this; + } + /** * Allows specifying the {@link PasswordEncoder} to use with the * {@link DaoAuthenticationProvider}. The default is to use plain text. diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java index ef729c5b4a4..d3a8770721d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java @@ -30,7 +30,7 @@ import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.core.NativeDetector; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.util.Assert; /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java index bb5147fb301..f969dcf4710 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java @@ -21,7 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java index a4e29505a74..68687038069 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java @@ -17,7 +17,6 @@ package org.springframework.security.config.annotation.method.configuration; import java.util.ArrayList; -import java.util.List; import org.aopalliance.intercept.MethodInterceptor; @@ -27,6 +26,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; +import org.springframework.security.aot.hint.AuthorizeReturnObjectCoreHintsRegistrar; +import org.springframework.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authorization.AuthorizationProxyFactory; import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor; @@ -37,13 +39,10 @@ final class AuthorizationProxyConfiguration implements AopInfrastructureBean { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider provider, + static AuthorizationAdvisorProxyFactory authorizationProxyFactory( ObjectProvider> customizers) { - List advisors = new ArrayList<>(); - provider.forEach(advisors::add); - AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(new ArrayList<>()); customizers.forEach((c) -> c.customize(factory)); - factory.setAdvisors(advisors); return factory; } @@ -51,13 +50,17 @@ static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider provider, AuthorizationAdvisorProxyFactory authorizationProxyFactory) { + provider.forEach(authorizationProxyFactory::addAdvisor); AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor( authorizationProxyFactory); - List advisors = new ArrayList<>(); - provider.forEach(advisors::add); - advisors.add(interceptor); - authorizationProxyFactory.setAdvisors(advisors); + authorizationProxyFactory.addAdvisor(interceptor); return interceptor; } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static SecurityHintsRegistrar authorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory) { + return new AuthorizeReturnObjectCoreHintsRegistrar(proxyFactory); + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java new file mode 100644 index 00000000000..e446ee27368 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar; + +@Configuration(proxyBeanMethods = false) +final class AuthorizationProxyDataConfiguration implements AopInfrastructureBean { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static SecurityHintsRegistrar authorizeReturnObjectDataHintsRegistrar(AuthorizationProxyFactory proxyFactory) { + return new AuthorizeReturnObjectDataHintsRegistrar(proxyFactory); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index 95e657aaa28..b20c5eab2a6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.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. @@ -27,7 +27,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; @@ -69,7 +68,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -84,6 +83,7 @@ * * @author Rob Winch * @author Eddú Meléndez + * @author Ngoc Nhan * @since 3.2 * @see EnableGlobalMethodSecurity * @deprecated Use {@link PrePostMethodSecurityConfiguration}, @@ -97,7 +97,7 @@ public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInit private static final Log logger = LogFactory.getLog(GlobalMethodSecurityConfiguration.class); - private ObjectPostProcessor objectPostProcessor = new ObjectPostProcessor() { + private ObjectPostProcessor objectPostProcessor = new ObjectPostProcessor<>() { @Override public T postProcess(T object) { @@ -168,19 +168,19 @@ public void afterSingletonsInstantiated() { catch (Exception ex) { throw new RuntimeException(ex); } - PermissionEvaluator permissionEvaluator = getSingleBeanOrNull(PermissionEvaluator.class); + PermissionEvaluator permissionEvaluator = getBeanOrNull(PermissionEvaluator.class); if (permissionEvaluator != null) { this.defaultMethodExpressionHandler.setPermissionEvaluator(permissionEvaluator); } - RoleHierarchy roleHierarchy = getSingleBeanOrNull(RoleHierarchy.class); + RoleHierarchy roleHierarchy = getBeanOrNull(RoleHierarchy.class); if (roleHierarchy != null) { this.defaultMethodExpressionHandler.setRoleHierarchy(roleHierarchy); } - AuthenticationTrustResolver trustResolver = getSingleBeanOrNull(AuthenticationTrustResolver.class); + AuthenticationTrustResolver trustResolver = getBeanOrNull(AuthenticationTrustResolver.class); if (trustResolver != null) { this.defaultMethodExpressionHandler.setTrustResolver(trustResolver); } - GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class); + GrantedAuthorityDefaults grantedAuthorityDefaults = getBeanOrNull(GrantedAuthorityDefaults.class); if (grantedAuthorityDefaults != null) { this.defaultMethodExpressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); } @@ -188,13 +188,8 @@ public void afterSingletonsInstantiated() { this.defaultMethodExpressionHandler = this.objectPostProcessor.postProcess(this.defaultMethodExpressionHandler); } - private T getSingleBeanOrNull(Class type) { - try { - return this.context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - } - return null; + private T getBeanOrNull(Class type) { + return this.context.getBeanProvider(type).getIfUnique(); } private void initializeMethodSecurityInterceptor() throws Exception { @@ -262,7 +257,7 @@ protected AccessDecisionManager accessDecisionManager() { decisionVoters.add(new Jsr250Voter()); } RoleVoter roleVoter = new RoleVoter(); - GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class); + GrantedAuthorityDefaults grantedAuthorityDefaults = getBeanOrNull(GrantedAuthorityDefaults.class); if (grantedAuthorityDefaults != null) { roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); } @@ -373,7 +368,7 @@ public MethodSecurityMetadataSource methodSecurityMetadataSource() { sources.add(new SecuredAnnotationSecurityMetadataSource()); } if (isJsr250Enabled) { - GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class); + GrantedAuthorityDefaults grantedAuthorityDefaults = getBeanOrNull(GrantedAuthorityDefaults.class); Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context .getBean(Jsr250MethodSecurityMetadataSource.class); if (grantedAuthorityDefaults != null) { @@ -412,6 +407,16 @@ public void setObjectPostProcessor(ObjectPostProcessor objectPostProcess this.objectPostProcessor = objectPostProcessor; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + @Autowired(required = false) + public void setObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + this.objectPostProcessor = objectPostProcessor; + } + @Autowired(required = false) public void setMethodSecurityExpressionHandler(List handlers) { if (handlers.size() != 1) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java index 97f529efd47..2d6f9a25258 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java @@ -18,7 +18,6 @@ import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -36,9 +35,9 @@ import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.Jsr250AuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -58,8 +57,15 @@ final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrast private final Jsr250AuthorizationManager authorizationManager = new Jsr250AuthorizationManager(); - private AuthorizationManagerBeforeMethodInterceptor methodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .jsr250(this.authorizationManager); + private final AuthorizationManagerBeforeMethodInterceptor methodInterceptor; + + Jsr250MethodSecurityConfiguration( + ObjectProvider>> postProcessors) { + ObjectPostProcessor> postProcessor = postProcessors + .getIfUnique(ObjectPostProcessor::identity); + AuthorizationManager manager = postProcessor.postProcess(this.authorizationManager); + this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.jsr250(manager); + } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @@ -95,16 +101,6 @@ void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityCont this.methodInterceptor.setSecurityContextHolderStrategy(securityContextHolderStrategy); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - AuthorizationManager observed = new ObservationAuthorizationManager<>(registry, - this.authorizationManager); - this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.secured(observed); - } - @Autowired(required = false) void setEventPublisher(AuthorizationEventPublisher eventPublisher) { this.methodInterceptor.setAuthorizationEventPublisher(eventPublisher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java new file mode 100644 index 00000000000..53bef440626 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class MethodObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index f8f5c8f1723..47d5d23f761 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; /** * Dynamically determines which imports to include using the {@link EnableMethodSecurity} @@ -37,6 +38,12 @@ */ final class MethodSecuritySelector implements ImportSelector { + private static final boolean isDataPresent = ClassUtils + .isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null); + + private static final boolean isObservabilityPresent = ClassUtils + .isPresent("io.micrometer.observation.ObservationRegistry", null); + private final ImportSelector autoProxy = new AutoProxyRegistrarSelector(); @Override @@ -57,6 +64,12 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) { imports.add(Jsr250MethodSecurityConfiguration.class.getName()); } imports.add(AuthorizationProxyConfiguration.class.getName()); + if (isDataPresent) { + imports.add(AuthorizationProxyDataConfiguration.class.getName()); + } + if (isObservabilityPresent) { + imports.add(MethodObservationConfiguration.class.getName()); + } return imports.toArray(new String[0]); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 9281e0ebf84..a1a47e3c54c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -16,8 +16,8 @@ package org.springframework.security.config.annotation.method.configuration; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; import org.springframework.aop.framework.AopInfrastructureBean; @@ -35,16 +35,21 @@ import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar; +import org.springframework.security.aot.hint.SecurityHintsRegistrar; import org.springframework.security.authorization.AuthorizationEventPublisher; -import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PrePostTemplateDefaults; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; /** @@ -75,21 +80,29 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, Applicati private final PreFilterAuthorizationMethodInterceptor preFilterMethodInterceptor = new PreFilterAuthorizationMethodInterceptor(); - private AuthorizationManagerBeforeMethodInterceptor preAuthorizeMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(this.preAuthorizeAuthorizationManager); + private final AuthorizationManagerBeforeMethodInterceptor preAuthorizeMethodInterceptor; - private AuthorizationManagerAfterMethodInterceptor postAuthorizeMethodInterceptor = AuthorizationManagerAfterMethodInterceptor - .postAuthorize(this.postAuthorizeAuthorizationManager); + private final AuthorizationManagerAfterMethodInterceptor postAuthorizeMethodInterceptor; private final PostFilterAuthorizationMethodInterceptor postFilterMethodInterceptor = new PostFilterAuthorizationMethodInterceptor(); private final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - { + PrePostMethodSecurityConfiguration( + ObjectProvider>> preAuthorizeProcessor, + ObjectProvider>> postAuthorizeProcessor) { this.preFilterMethodInterceptor.setExpressionHandler(this.expressionHandler); this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); this.postAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); this.postFilterMethodInterceptor.setExpressionHandler(this.expressionHandler); + AuthorizationManager preAuthorize = preAuthorizeProcessor + .getIfUnique(ObjectPostProcessor::identity) + .postProcess(this.preAuthorizeAuthorizationManager); + this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize(preAuthorize); + AuthorizationManager postAuthorize = postAuthorizeProcessor + .getIfUnique(ObjectPostProcessor::identity) + .postProcess(this.postAuthorizeAuthorizationManager); + this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterMethodInterceptor.postAuthorize(postAuthorize); } @Override @@ -109,6 +122,14 @@ void setRoleHierarchy(RoleHierarchy roleHierarchy) { this.expressionHandler.setRoleHierarchy(roleHierarchy); } + @Autowired(required = false) + void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults); + this.preAuthorizeAuthorizationManager.setTemplateDefaults(templateDefaults); + this.postAuthorizeAuthorizationManager.setTemplateDefaults(templateDefaults); + this.postFilterMethodInterceptor.setTemplateDefaults(templateDefaults); + } + @Autowired(required = false) void setTemplateDefaults(PrePostTemplateDefaults templateDefaults) { this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults); @@ -133,17 +154,6 @@ void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityCont this.postFilterMethodInterceptor.setSecurityContextHolderStrategy(securityContextHolderStrategy); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(new ObservationAuthorizationManager<>(registry, this.preAuthorizeAuthorizationManager)); - this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterMethodInterceptor - .postAuthorize(new ObservationAuthorizationManager<>(registry, this.postAuthorizeAuthorizationManager)); - } - @Autowired(required = false) void setAuthorizationEventPublisher(AuthorizationEventPublisher publisher) { this.preAuthorizeMethodInterceptor.setAuthorizationEventPublisher(publisher); @@ -182,6 +192,12 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor( () -> _prePostMethodSecurityConfiguration.getObject().postFilterMethodInterceptor); } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static SecurityHintsRegistrar prePostAuthorizeExpressionHintsRegistrar() { + return new PrePostAuthorizeHintsRegistrar(); + } + @Override public void setImportMetadata(AnnotationMetadata importMetadata) { EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 8e777e8bc4c..7373d7b8c4e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -16,30 +16,25 @@ package org.springframework.security.config.annotation.method.configuration; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import io.micrometer.observation.ObservationRegistry; -import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.Role; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager; -import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; import org.springframework.security.authorization.method.MethodInvocationResult; @@ -48,8 +43,9 @@ import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager; import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PrePostTemplateDefaults; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.core.GrantedAuthorityDefaults; -import org.springframework.util.function.SingletonSupplier; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; /** * Configuration for a {@link ReactiveAuthenticationManager} based Method Security. @@ -57,63 +53,115 @@ * @author Evgeniy Cheban * @since 5.8 */ -@Configuration(proxyBeanMethods = false) -final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements AopInfrastructureBean { +@Configuration(value = "_reactiveMethodSecurityConfiguration", proxyBeanMethods = false) +final class ReactiveAuthorizationManagerMethodSecurityConfiguration + implements AopInfrastructureBean, ApplicationContextAware { + + private static final Pointcut preFilterPointcut = new PreFilterAuthorizationReactiveMethodInterceptor() + .getPointcut(); + + private static final Pointcut preAuthorizePointcut = AuthorizationManagerBeforeReactiveMethodInterceptor + .preAuthorize() + .getPointcut(); + + private static final Pointcut postAuthorizePointcut = AuthorizationManagerAfterReactiveMethodInterceptor + .postAuthorize() + .getPointcut(); + + private static final Pointcut postFilterPointcut = new PostFilterAuthorizationReactiveMethodInterceptor() + .getPointcut(); + + private PreFilterAuthorizationReactiveMethodInterceptor preFilterMethodInterceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + + private PreAuthorizeReactiveAuthorizationManager preAuthorizeAuthorizationManager = new PreAuthorizeReactiveAuthorizationManager(); + + private PostAuthorizeReactiveAuthorizationManager postAuthorizeAuthorizationManager = new PostAuthorizeReactiveAuthorizationManager(); + + private PostFilterAuthorizationReactiveMethodInterceptor postFilterMethodInterceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + + private final AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeMethodInterceptor; + + private final AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeMethodInterceptor; + + @Autowired(required = false) + ReactiveAuthorizationManagerMethodSecurityConfiguration(MethodSecurityExpressionHandler expressionHandler, + ObjectProvider>> preAuthorizePostProcessor, + ObjectProvider>> postAuthorizePostProcessor) { + if (expressionHandler != null) { + this.preFilterMethodInterceptor = new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler); + this.preAuthorizeAuthorizationManager = new PreAuthorizeReactiveAuthorizationManager(expressionHandler); + this.postFilterMethodInterceptor = new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler); + this.postAuthorizeAuthorizationManager = new PostAuthorizeReactiveAuthorizationManager(expressionHandler); + } + ReactiveAuthorizationManager preAuthorize = preAuthorizePostProcessor + .getIfUnique(ObjectPostProcessor::identity) + .postProcess(this.preAuthorizeAuthorizationManager); + this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeReactiveMethodInterceptor + .preAuthorize(preAuthorize); + ReactiveAuthorizationManager postAuthorize = postAuthorizePostProcessor + .getIfAvailable(ObjectPostProcessor::identity) + .postProcess(this.postAuthorizeAuthorizationManager); + this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterReactiveMethodInterceptor + .postAuthorize(postAuthorize); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.preAuthorizeAuthorizationManager.setApplicationContext(context); + this.postAuthorizeAuthorizationManager.setApplicationContext(context); + } + + @Autowired(required = false) + void setTemplateDefaults(PrePostTemplateDefaults templateDefaults) { + this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults); + this.preAuthorizeAuthorizationManager.setTemplateDefaults(templateDefaults); + this.postAuthorizeAuthorizationManager.setTemplateDefaults(templateDefaults); + this.postFilterMethodInterceptor.setTemplateDefaults(templateDefaults); + } + + @Autowired(required = false) + void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults); + this.preAuthorizeAuthorizationManager.setTemplateDefaults(templateDefaults); + this.postAuthorizeAuthorizationManager.setTemplateDefaults(templateDefaults); + this.postFilterMethodInterceptor.setTemplateDefaults(templateDefaults); + } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler, - ObjectProvider defaultsObjectProvider) { - PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor( - expressionHandler); - return new DeferringMethodInterceptor<>(interceptor, - (i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults)); + static MethodInterceptor preFilterAuthorizationMethodInterceptor( + ObjectProvider _reactiveMethodSecurityConfiguration) { + return new DeferringMethodInterceptor<>(preFilterPointcut, + () -> _reactiveMethodSecurityConfiguration.getObject().preFilterMethodInterceptor); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( - MethodSecurityExpressionHandler expressionHandler, - ObjectProvider defaultsObjectProvider, - ObjectProvider registryProvider, ApplicationContext context) { - PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager( - expressionHandler); - manager.setApplicationContext(context); - ReactiveAuthorizationManager authorizationManager = manager(manager, registryProvider); - AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor - .preAuthorize(authorizationManager); - return new DeferringMethodInterceptor<>(interceptor, - (i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults)); + ObjectProvider _reactiveMethodSecurityConfiguration) { + return new DeferringMethodInterceptor<>(preAuthorizePointcut, + () -> _reactiveMethodSecurityConfiguration.getObject().preAuthorizeMethodInterceptor); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler, - ObjectProvider defaultsObjectProvider) { - PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor( - expressionHandler); - return new DeferringMethodInterceptor<>(interceptor, - (i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults)); + static MethodInterceptor postFilterAuthorizationMethodInterceptor( + ObjectProvider _reactiveMethodSecurityConfiguration) { + return new DeferringMethodInterceptor<>(postFilterPointcut, + () -> _reactiveMethodSecurityConfiguration.getObject().postFilterMethodInterceptor); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( - MethodSecurityExpressionHandler expressionHandler, - ObjectProvider defaultsObjectProvider, - ObjectProvider registryProvider, ApplicationContext context) { - PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager( - expressionHandler); - manager.setApplicationContext(context); - ReactiveAuthorizationManager authorizationManager = manager(manager, registryProvider); - AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor - .postAuthorize(authorizationManager); - return new DeferringMethodInterceptor<>(interceptor, - (i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults)); + ObjectProvider _reactiveMethodSecurityConfiguration) { + return new DeferringMethodInterceptor<>(postAuthorizePointcut, + () -> _reactiveMethodSecurityConfiguration.getObject().postAuthorizeMethodInterceptor); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Fallback static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( @Autowired(required = false) GrantedAuthorityDefaults grantedAuthorityDefaults) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); @@ -123,55 +171,4 @@ static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( return handler; } - static ReactiveAuthorizationManager manager(ReactiveAuthorizationManager delegate, - ObjectProvider registryProvider) { - return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate); - } - - private static final class DeferringMethodInterceptor - implements AuthorizationAdvisor { - - private final Pointcut pointcut; - - private final int order; - - private final Supplier delegate; - - DeferringMethodInterceptor(M delegate, Consumer supplier) { - this.pointcut = delegate.getPointcut(); - this.order = delegate.getOrder(); - this.delegate = SingletonSupplier.of(() -> { - supplier.accept(delegate); - return delegate; - }); - } - - @Nullable - @Override - public Object invoke(@NotNull MethodInvocation invocation) throws Throwable { - return this.delegate.get().invoke(invocation); - } - - @Override - public Pointcut getPointcut() { - return this.pointcut; - } - - @Override - public Advice getAdvice() { - return this; - } - - @Override - public int getOrder() { - return this.order; - } - - @Override - public boolean isPerInstance() { - return true; - } - - } - } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java deleted file mode 100644 index 7912991c4fb..00000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.method.configuration; - -import java.util.ArrayList; -import java.util.List; - -import org.aopalliance.intercept.MethodInterceptor; - -import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.security.authorization.method.AuthorizationAdvisor; -import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; -import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor; -import org.springframework.security.config.Customizer; - -@Configuration(proxyBeanMethods = false) -final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean { - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider provider, - ObjectProvider> customizers) { - List advisors = new ArrayList<>(); - provider.forEach(advisors::add); - AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withReactiveDefaults(); - customizers.forEach((c) -> c.customize(factory)); - factory.setAdvisors(advisors); - return factory; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider provider, - AuthorizationAdvisorProxyFactory authorizationProxyFactory) { - AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor( - authorizationProxyFactory); - List advisors = new ArrayList<>(); - provider.forEach(advisors::add); - advisors.add(interceptor); - authorizationProxyFactory.setAdvisors(advisors); - return interceptor; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java new file mode 100644 index 00000000000..3f73480cb7b --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveMethodObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java index 7d1d241f16a..41f356772f3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.Role; import org.springframework.core.type.AnnotationMetadata; @@ -82,6 +83,7 @@ static PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(Abstract @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Fallback static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( ReactiveMethodSecurityConfiguration configuration) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java index b1c923383e5..c204b33aaae 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; /** * @author Rob Winch @@ -34,6 +35,12 @@ */ class ReactiveMethodSecuritySelector implements ImportSelector { + private static final boolean isDataPresent = ClassUtils + .isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null); + + private static final boolean isObservabilityPresent = ClassUtils + .isPresent("io.micrometer.observation.ObservationRegistry", null); + private final ImportSelector autoProxy = new AutoProxyRegistrarSelector(); @Override @@ -51,7 +58,13 @@ public String[] selectImports(AnnotationMetadata importMetadata) { else { imports.add(ReactiveMethodSecurityConfiguration.class.getName()); } - imports.add(ReactiveAuthorizationProxyConfiguration.class.getName()); + if (isDataPresent) { + imports.add(AuthorizationProxyDataConfiguration.class.getName()); + } + if (isObservabilityPresent) { + imports.add(ReactiveMethodObservationConfiguration.class.getName()); + } + imports.add(AuthorizationProxyConfiguration.class.getName()); return imports.toArray(new String[0]); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java index 7247e8044db..3230996d6f5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java @@ -18,7 +18,6 @@ import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -37,9 +36,9 @@ import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.SecuredAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.context.SecurityContextHolderStrategy; /** @@ -58,8 +57,15 @@ final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfras private final SecuredAuthorizationManager authorizationManager = new SecuredAuthorizationManager(); - private AuthorizationManagerBeforeMethodInterceptor methodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .secured(this.authorizationManager); + private final AuthorizationManagerBeforeMethodInterceptor methodInterceptor; + + SecuredMethodSecurityConfiguration( + ObjectProvider>> postProcessors) { + ObjectPostProcessor> postProcessor = postProcessors + .getIfUnique(ObjectPostProcessor::identity); + AuthorizationManager manager = postProcessor.postProcess(this.authorizationManager); + this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.secured(manager); + } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @@ -90,16 +96,6 @@ void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityCont this.methodInterceptor.setSecurityContextHolderStrategy(securityContextHolderStrategy); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - AuthorizationManager observed = new ObservationAuthorizationManager<>(registry, - this.authorizationManager); - this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.secured(observed); - } - @Autowired(required = false) void setEventPublisher(AuthorizationEventPublisher eventPublisher) { this.methodInterceptor.setAuthorizationEventPublisher(eventPublisher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java index 29058c10e69..ab46f90bd02 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java @@ -35,7 +35,8 @@ @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Import({ RSocketSecurityConfiguration.class, SecuritySocketAcceptorInterceptorConfiguration.class }) +@Import({ RSocketSecurityConfiguration.class, SecuritySocketAcceptorInterceptorConfiguration.class, + ReactiveObservationImportSelector.class }) public @interface EnableRSocketSecurity { } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java index 50b7ae5f975..c868b29ba33 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-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. @@ -108,6 +108,7 @@ * @author Luis Felipe Vega * @author Manuel Tejeda * @author Ebert Toribio + * @author Ngoc Nhan * @since 5.2 */ public class RSocketSecurity { @@ -238,15 +239,12 @@ private T getBeanOrNull(Class beanClass) { return getBeanOrNull(ResolvableType.forClass(beanClass)); } + @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { if (this.context == null) { return null; } - String[] names = this.context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) this.context.getBean(names[0]); - } - return null; + return (T) this.context.getBeanProvider(type).getIfUnique(); } protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException { diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java index 9f7f5c9c5ba..eaa0823408a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-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. @@ -16,16 +16,16 @@ package org.springframework.security.config.annotation.rsocket; -import io.micrometer.observation.ObservationRegistry; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; -import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -46,7 +46,7 @@ class RSocketSecurityConfiguration { private PasswordEncoder passwordEncoder; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor postProcessor = ObjectPostProcessor.identity(); @Autowired(required = false) void setAuthenticationManager(ReactiveAuthenticationManager authenticationManager) { @@ -64,8 +64,12 @@ void setPasswordEncoder(PasswordEncoder passwordEncoder) { } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setAuthenticationManagerPostProcessor( + Map> postProcessors) { + if (postProcessors.size() == 1) { + this.postProcessor = postProcessors.values().iterator().next(); + } + this.postProcessor = postProcessors.get("rSocketAuthenticationManagerPostProcessor"); } @Bean(name = RSOCKET_SECURITY_BEAN_NAME) @@ -86,10 +90,7 @@ private ReactiveAuthenticationManager authenticationManager() { if (this.passwordEncoder != null) { manager.setPasswordEncoder(this.passwordEncoder); } - if (!this.observationRegistry.isNoop()) { - return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager); - } - return manager; + return this.postProcessor.postProcess(manager); } return null; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java new file mode 100644 index 00000000000..b8d38804749 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.rsocket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.rsocket.api.PayloadExchange; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> rSocketAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor rSocketAuthenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthenticationManager postProcess(ReactiveAuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationReactiveAuthenticationManager(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java new file mode 100644 index 00000000000..6e18bc4396c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.rsocket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebFluxSecurity} to conditionally import observation configuration + * when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class ReactiveObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = ReactiveObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { ReactiveObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 0f779f2df32..d94e9d9083e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -34,10 +34,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -113,7 +115,9 @@ public C anyRequest() { */ protected final List createMvcMatchers(HttpMethod method, String... mvcPatterns) { Assert.state(!this.anyRequestConfigured, "Can't configure mvcMatchers after anyRequest"); - ObjectPostProcessor opp = this.context.getBean(ObjectPostProcessor.class); + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); + ObjectProvider> postProcessors = this.context.getBeanProvider(type); + ObjectPostProcessor opp = postProcessors.getObject(); if (!this.context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type " + HandlerMappingIntrospector.class.getName() @@ -195,6 +199,12 @@ public C requestMatchers(RequestMatcher... requestMatchers) { * @since 5.8 */ public C requestMatchers(HttpMethod method, String... patterns) { + if (anyPathsDontStartWithLeadingSlash(patterns)) { + this.logger.warn("One of the patterns in " + Arrays.toString(patterns) + + " is missing a leading slash. This is discouraged; please include the " + + "leading slash in all your request matcher patterns. In future versions of " + + "Spring Security, leaving out the leading slash will result in an exception."); + } if (!mvcPresent) { return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); } @@ -215,6 +225,15 @@ public C requestMatchers(HttpMethod method, String... patterns) { return requestMatchers(matchers.toArray(new RequestMatcher[0])); } + private boolean anyPathsDontStartWithLeadingSlash(String... patterns) { + for (String pattern : patterns) { + if (!pattern.startsWith("/")) { + return true; + } + } + return false; + } + private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) { Map registrations = mappableServletRegistrations(servletContext); if (registrations.isEmpty()) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java index 94c709f5b7d..89a61eec9fa 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java @@ -157,6 +157,7 @@ public interface HttpSecurityBuilder> *
  • {@link DigestAuthenticationFilter}
  • *
  • {@link BearerTokenAuthenticationFilter}
  • *
  • {@link BasicAuthenticationFilter}
  • + *
  • {@link org.springframework.security.web.authentication.AuthenticationFilter}
  • *
  • {@link RequestCacheAwareFilter}
  • *
  • {@link SecurityContextHolderAwareRequestFilter}
  • *
  • {@link JaasApiIntegrationFilter}
  • diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index 1604bff2fe5..6f297cdb231 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java @@ -27,14 +27,18 @@ import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.AuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; import org.springframework.security.web.context.SecurityContextHolderFilter; @@ -87,6 +91,7 @@ final class FilterOrderRegistration { this.filterToOrder.put( "org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter", order.next()); + put(GenerateOneTimeTokenFilter.class, order.next()); put(X509AuthenticationFilter.class, order.next()); put(AbstractPreAuthenticatedProcessingFilter.class, order.next()); this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next()); @@ -97,14 +102,17 @@ final class FilterOrderRegistration { order.next()); put(UsernamePasswordAuthenticationFilter.class, order.next()); order.next(); // gh-8105 + put(DefaultResourcesFilter.class, order.next()); put(DefaultLoginPageGeneratingFilter.class, order.next()); put(DefaultLogoutPageGeneratingFilter.class, order.next()); + put(DefaultOneTimeTokenSubmitPageGeneratingFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); put(DigestAuthenticationFilter.class, order.next()); this.filterToOrder.put( "org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter", order.next()); put(BasicAuthenticationFilter.class, order.next()); + put(AuthenticationFilter.class, order.next()); put(RequestCacheAwareFilter.class, order.next()); put(SecurityContextHolderAwareRequestFilter.class, order.next()); put(JaasApiIntegrationFilter.class, order.next()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 9d0333d24ee..f96c943d557 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; -import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -30,15 +29,16 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.ObservationAuthenticationManager; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; @@ -67,11 +67,13 @@ import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer; import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer; import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; +import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer; import org.springframework.security.config.annotation.web.configurers.X509Configurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.annotation.web.configurers.ott.OneTimeTokenLoginConfigurer; import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer; import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer; import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer; @@ -138,6 +140,7 @@ * * @author Rob Winch * @author Joe Grandja + * @author Ngoc Nhan * @since 3.2 * @see EnableWebSecurity */ @@ -185,6 +188,23 @@ public HttpSecurity(ObjectPostProcessor objectPostProcessor, this.requestMatcherConfigurer = new RequestMatcherConfigurer(context); } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + @SuppressWarnings("unchecked") + public HttpSecurity(org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor, + AuthenticationManagerBuilder authenticationBuilder, Map, Object> sharedObjects) { + super(objectPostProcessor); + Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null"); + setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder); + for (Map.Entry, Object> entry : sharedObjects.entrySet()) { + setSharedObject((Class) entry.getKey(), entry.getValue()); + } + ApplicationContext context = (ApplicationContext) sharedObjects.get(ApplicationContext.class); + this.requestMatcherConfigurer = new RequestMatcherConfigurer(context); + } + private ApplicationContext getContext() { return getSharedObject(ApplicationContext.class); } @@ -1111,9 +1131,10 @@ public HttpSecurity rememberMe(Customizer> re * * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests()} instead + * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests(Customizer)} + * instead */ - @Deprecated + @Deprecated(since = "6.1", forRemoval = true) public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { ApplicationContext context = getContext(); @@ -1226,9 +1247,10 @@ public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrl * for the {@link ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry} * @return the {@link HttpSecurity} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests} instead + * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests(Customizer)} + * instead */ - @Deprecated + @Deprecated(since = "6.1", forRemoval = true) public HttpSecurity authorizeRequests( Customizer.ExpressionInterceptUrlRegistry> authorizeRequestsCustomizer) throws Exception { @@ -2978,6 +3000,45 @@ public HttpSecurity oauth2ResourceServer( return HttpSecurity.this; } + /** + * Configures One-Time Token Login Support. + * + *

    Example Configuration

    + * + *
    +	 * @Configuration
    +	 * @EnableWebSecurity
    +	 * public class SecurityConfig {
    +	 *
    +	 * 	@Bean
    +	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    +	 * 		http
    +	 * 			.authorizeHttpRequests((authorize) -> authorize
    +	 * 					.anyRequest().authenticated()
    +	 * 			)
    +	 * 			.oneTimeTokenLogin(Customizer.withDefaults());
    +	 * 		return http.build();
    +	 * 	}
    +	 *
    +	 * 	@Bean
    +	 * 	public OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
    +	 * 		return new MyMagicLinkOneTimeTokenGenerationSuccessHandler();
    +	 * 	}
    +	 *
    +	 * }
    +	 * 
    + * @param oneTimeTokenLoginConfigurerCustomizer the {@link Customizer} to provide more + * options for the {@link OneTimeTokenLoginConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity oneTimeTokenLogin( + Customizer> oneTimeTokenLoginConfigurerCustomizer) + throws Exception { + oneTimeTokenLoginConfigurerCustomizer.customize(getOrApply(new OneTimeTokenLoginConfigurer<>(getContext()))); + return HttpSecurity.this; + } + /** * Configures channel security. In order for this configuration to be useful at least * one mapping to a required channel must be provided. @@ -3236,13 +3297,10 @@ protected void beforeConfigure() throws Exception { setSharedObject(AuthenticationManager.class, this.authenticationManager); } else { - ObservationRegistry registry = getObservationRegistry(); + ObjectPostProcessor postProcessor = getAuthenticationManagerPostProcessor(); AuthenticationManager manager = getAuthenticationRegistry().build(); - if (!registry.isNoop() && manager != null) { - setSharedObject(AuthenticationManager.class, new ObservationAuthenticationManager(registry, manager)); - } - else { - setSharedObject(AuthenticationManager.class, manager); + if (manager != null) { + setSharedObject(AuthenticationManager.class, postProcessor.postProcess(manager)); } } } @@ -3634,6 +3692,31 @@ public HttpSecurity securityMatcher(String... patterns) { return this; } + /** + * Specifies webAuthn/passkeys based authentication. + * + *
    +	 * 	@Bean
    +	 * 	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    +	 * 		http
    +	 * 			// ...
    +	 * 			.webAuthn((webAuthn) -> webAuthn
    +	 * 				.rpName("Spring Security Relying Party")
    +	 * 				.rpId("example.com")
    +	 * 				.allowedOrigins("https://example.com")
    +	 * 			);
    +	 * 		return http.build();
    +	 * 	}
    +	 * 
    + * @param webAuthn the customizer to apply + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity webAuthn(Customizer> webAuthn) throws Exception { + webAuthn.customize(getOrApply(new WebAuthnConfigurer<>())); + return HttpSecurity.this; + } + private List createAntMatchers(String... patterns) { List matchers = new ArrayList<>(patterns.length); for (String pattern : patterns) { @@ -3643,7 +3726,9 @@ private List createAntMatchers(String... patterns) { } private List createMvcMatchers(String... mvcPatterns) { - ObjectPostProcessor opp = getContext().getBean(ObjectPostProcessor.class); + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); + ObjectProvider> postProcessors = getContext().getBeanProvider(type); + ObjectPostProcessor opp = postProcessors.getObject(); if (!getContext().containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type " + HandlerMappingIntrospector.class.getName() @@ -3678,13 +3763,12 @@ private getAuthenticationManagerPostProcessor() { ApplicationContext context = getContext(); - String[] names = context.getBeanNamesForType(ObservationRegistry.class); - if (names.length == 1) { - return (ObservationRegistry) context.getBean(names[0]); - } - return ObservationRegistry.NOOP; + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + AuthenticationManager.class); + ObjectProvider> manager = context.getBeanProvider(type); + return manager.getIfUnique(ObjectPostProcessor::identity); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 504322f5a20..aadf4302d39 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -28,14 +28,16 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; @@ -45,8 +47,8 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.FilterChainProxy.FilterChainDecorator; import org.springframework.security.web.FilterInvocation; -import org.springframework.security.web.ObservationFilterChainDecorator; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer; @@ -63,6 +65,7 @@ import org.springframework.security.web.firewall.ObservationMarkingRequestRejectedHandler; import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; @@ -109,6 +112,9 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder filterChainDecoratorPostProcessor = ObjectPostProcessor + .identity(); + private HttpServletRequestTransformer privilegeEvaluatorRequestTransformer; private DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler(); @@ -129,6 +135,14 @@ public WebSecurity(ObjectPostProcessor objectPostProcessor) { super(objectPostProcessor); } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public WebSecurity(org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor); + } + /** *

    * Allows adding {@link RequestMatcher} instances that Spring Security should ignore. @@ -296,8 +310,20 @@ protected Filter performBuild() throws Exception { requestMatcherPrivilegeEvaluatorsEntries .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain)); } + DefaultSecurityFilterChain anyRequestFilterChain = null; for (SecurityBuilder securityFilterChainBuilder : this.securityFilterChainBuilders) { SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build(); + if (anyRequestFilterChain != null) { + String message = "A filter chain that matches any request [" + anyRequestFilterChain + + "] has already been configured, which means that this filter chain [" + securityFilterChain + + "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last."; + throw new IllegalArgumentException(message); + } + if (securityFilterChain instanceof DefaultSecurityFilterChain defaultSecurityFilterChain) { + if (defaultSecurityFilterChain.getRequestMatcher() instanceof AnyRequestMatcher) { + anyRequestFilterChain = defaultSecurityFilterChain; + } + } securityFilterChains.add(securityFilterChain); requestMatcherPrivilegeEvaluatorsEntries .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain)); @@ -392,6 +418,11 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } catch (NoSuchBeanDefinitionException ex) { } + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + FilterChainDecorator.class); + ObjectProvider> postProcessor = applicationContext + .getBeanProvider(type); + this.filterChainDecoratorPostProcessor = postProcessor.getIfUnique(ObjectPostProcessor::identity); Class requestTransformerClass = HttpServletRequestTransformer.class; this.privilegeEvaluatorRequestTransformer = applicationContext.getBeanProvider(requestTransformerClass) .getIfUnique(); @@ -402,11 +433,8 @@ public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } - FilterChainProxy.FilterChainDecorator getFilterChainDecorator() { - if (this.observationRegistry.isNoop()) { - return new FilterChainProxy.VirtualFilterChainDecorator(); - } - return new ObservationFilterChainDecorator(this.observationRegistry); + FilterChainDecorator getFilterChainDecorator() { + return this.filterChainDecoratorPostProcessor.postProcess(new FilterChainProxy.VirtualFilterChainDecorator()); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index d00242377e2..84706ebae46 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -82,7 +82,7 @@ @Target(ElementType.TYPE) @Documented @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, - HttpSecurityConfiguration.class }) + HttpSecurityConfiguration.class, ObservationImportSelector.class }) @EnableGlobalAuthentication public @interface EnableWebSecurity { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 39a3633db4c..3cd79c99670 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -30,7 +29,7 @@ import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; @@ -56,6 +55,7 @@ * * @author Eleftheria Stein * @author Jinwoo Bae + * @author Ngoc Nhan * @since 5.4 */ @Configuration(proxyBeanMethods = false) @@ -178,6 +178,17 @@ static class DefaultPasswordEncoderAuthenticationManagerBuilder extends Authenti this.defaultPasswordEncoder = defaultPasswordEncoder; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + DefaultPasswordEncoderAuthenticationManagerBuilder( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor, + PasswordEncoder defaultPasswordEncoder) { + super(objectPostProcessor); + this.defaultPasswordEncoder = defaultPasswordEncoder; + } + @Override public InMemoryUserDetailsManagerConfigurer inMemoryAuthentication() throws Exception { @@ -226,21 +237,9 @@ private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; } - PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); - if (passwordEncoder == null) { - passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - } - this.passwordEncoder = passwordEncoder; - return passwordEncoder; - } - - private T getBeanOrNull(Class type) { - try { - return this.applicationContext.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + this.passwordEncoder = this.applicationContext.getBeanProvider(PasswordEncoder.class) + .getIfUnique(PasswordEncoderFactories::createDelegatingPasswordEncoder); + return this.passwordEncoder; } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 24bd18f75a5..13c9a1b3c07 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.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. @@ -29,7 +29,6 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -118,11 +117,19 @@ OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() { @Configuration(proxyBeanMethods = false) static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { - private OAuth2AuthorizedClientManager authorizedClientManager; + private final OAuth2AuthorizedClientManager authorizedClientManager; - private SecurityContextHolderStrategy securityContextHolderStrategy; + private final ObjectProvider securityContextHolderStrategy; - private OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + private final OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + + OAuth2ClientWebMvcSecurityConfiguration(ObjectProvider authorizedClientManager, + ObjectProvider securityContextHolderStrategy, + OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { + this.authorizedClientManager = authorizedClientManager.getIfUnique(); + this.securityContextHolderStrategy = securityContextHolderStrategy; + this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; + } @Override public void addArgumentResolvers(List argumentResolvers) { @@ -130,31 +137,11 @@ public void addArgumentResolvers(List argumentRes if (authorizedClientManager != null) { OAuth2AuthorizedClientArgumentResolver resolver = new OAuth2AuthorizedClientArgumentResolver( authorizedClientManager); - if (this.securityContextHolderStrategy != null) { - resolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); - } + this.securityContextHolderStrategy.ifAvailable(resolver::setSecurityContextHolderStrategy); argumentResolvers.add(resolver); } } - @Autowired(required = false) - void setAuthorizedClientManager(List authorizedClientManagers) { - if (authorizedClientManagers.size() == 1) { - this.authorizedClientManager = authorizedClientManagers.get(0); - } - } - - @Autowired(required = false) - void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) { - this.securityContextHolderStrategy = strategy; - } - - @Autowired - void setAuthorizedClientManagerRegistrar( - OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { - this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; - } - private OAuth2AuthorizedClientManager getAuthorizedClientManager() { if (this.authorizedClientManager != null) { return this.authorizedClientManager; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java new file mode 100644 index 00000000000..6ffee81a2ce --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configuration; + +import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ObservationAuthenticationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.FilterChainProxy.FilterChainDecorator; +import org.springframework.security.web.ObservationFilterChainDecorator; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthenticationManager postProcess(AuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public FilterChainDecorator postProcess(FilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java new file mode 100644 index 00000000000..202d150f2f5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configuration; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebSecurity} to conditionally import observation configuration + * when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class ObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = ObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { ObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java index ec33c902f53..3f03140a461 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.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. @@ -40,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.expression.BeanResolver; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.web.FilterChainProxy; @@ -82,18 +83,22 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private AnnotationTemplateExpressionDefaults templateDefaults; + @Override @SuppressWarnings("deprecation") public void addArgumentResolvers(List argumentResolvers) { AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver(); authenticationPrincipalResolver.setBeanResolver(this.beanResolver); authenticationPrincipalResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + authenticationPrincipalResolver.setTemplateDefaults(this.templateDefaults); argumentResolvers.add(authenticationPrincipalResolver); argumentResolvers .add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver()); CurrentSecurityContextArgumentResolver currentSecurityContextArgumentResolver = new CurrentSecurityContextArgumentResolver(); currentSecurityContextArgumentResolver.setBeanResolver(this.beanResolver); currentSecurityContextArgumentResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + currentSecurityContextArgumentResolver.setTemplateDefaults(this.templateDefaults); argumentResolvers.add(currentSecurityContextArgumentResolver); argumentResolvers.add(new CsrfTokenArgumentResolver()); } @@ -109,6 +114,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws if (applicationContext.getBeanNamesForType(SecurityContextHolderStrategy.class).length == 1) { this.securityContextHolderStrategy = applicationContext.getBean(SecurityContextHolderStrategy.class); } + if (applicationContext.getBeanNamesForType(AnnotationTemplateExpressionDefaults.class).length == 1) { + this.templateDefaults = applicationContext.getBean(AnnotationTemplateExpressionDefaults.class); + } } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 7274923af78..d172a85d596 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -38,7 +38,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -79,9 +79,6 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa private ClassLoader beanClassLoader; - @Autowired(required = false) - private ObjectPostProcessor objectObjectPostProcessor; - @Autowired(required = false) private HttpSecurity httpSecurity; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java index db4271329be..841783c4f62 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.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. @@ -17,7 +17,7 @@ package org.springframework.security.config.annotation.web.configurers; import org.springframework.context.ApplicationContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -54,6 +54,17 @@ public T withObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { return (T) this; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + @SuppressWarnings("unchecked") + public T withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return (T) this; + } + protected SecurityContextHolderStrategy getSecurityContextHolderStrategy() { if (this.securityContextHolderStrategy != null) { return this.securityContextHolderStrategy; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 39ca10124e5..d3f76a3f51e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -20,10 +20,11 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; @@ -32,9 +33,8 @@ import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManagers; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -68,6 +68,9 @@ public final class AuthorizeHttpRequestsConfigurer> postProcessor = ObjectPostProcessor + .identity(); + /** * Creates an instance. * @param context the {@link ApplicationContext} to use @@ -87,6 +90,11 @@ public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class); this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); } + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + ResolvableType.forClassWithGenerics(AuthorizationManager.class, HttpServletRequest.class)); + ObjectProvider>> provider = context + .getBeanProvider(type); + provider.ifUnique((postProcessor) -> this.postProcessor = postProcessor); } /** @@ -123,17 +131,6 @@ AuthorizationManagerRequestMatcherRegistry addFirst(RequestMatcher matcher, return this.registry; } - private ObservationRegistry getObservationRegistry() { - ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - String[] names = context.getBeanNamesForType(ObservationRegistry.class); - if (names.length == 1) { - return context.getBean(ObservationRegistry.class); - } - else { - return ObservationRegistry.NOOP; - } - } - /** * Registry for mapping a {@link RequestMatcher} to an {@link AuthorizationManager}. * @@ -173,13 +170,9 @@ private AuthorizationManager createAuthorizationManager() { + ". Try completing it with something like requestUrls()..hasRole('USER')"); Assert.state(this.mappingCount > 0, "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); - ObservationRegistry registry = getObservationRegistry(); AuthorizationManager manager = postProcess( (AuthorizationManager) this.managerBuilder.build()); - if (registry.isNoop()) { - return manager; - } - return new ObservationAuthorizationManager<>(registry, manager); + return AuthorizeHttpRequestsConfigurer.this.postProcessor.postProcess(manager); } @Override @@ -200,6 +193,16 @@ public AuthorizationManagerRequestMatcherRegistry withObjectPostProcessor( return this; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public AuthorizationManagerRequestMatcherRegistry withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return this; + } + /** * Sets whether all dispatcher types should be filtered. * @param shouldFilter should filter all dispatcher types. Default is {@code true} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index 241ef194763..07c49323b8e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -25,7 +25,7 @@ import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -169,6 +169,16 @@ public ChannelRequestMatcherRegistry withObjectPostProcessor(ObjectPostProcessor return this; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public ChannelRequestMatcherRegistry withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return this; + } + /** * Sets the {@link ChannelProcessor} instances to use in * {@link ChannelDecisionManagerImpl} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java index 4e955e36882..af024ee2d83 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.csrf.CsrfToken; /** @@ -98,6 +99,7 @@ public void configure(H http) { if (this.loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) { this.loginPageGeneratingFilter = postProcess(this.loginPageGeneratingFilter); http.addFilter(this.loginPageGeneratingFilter); + http.addFilter(DefaultResourcesFilter.css()); LogoutConfigurer logoutConfigurer = http.getConfigurer(LogoutConfigurer.class); if (logoutConfigurer != null) { http.addFilter(this.logoutPageGeneratingFilter); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java index ac7bdcaf544..599c9703d92 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java @@ -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. @@ -29,7 +29,7 @@ import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -75,6 +75,7 @@ * @param the type of {@link HttpSecurityBuilder} that is being configured * @author Rob Winch * @author Yanming Zhou + * @author Ngoc Nhan * @since 3.2 * @see org.springframework.security.config.annotation.web.builders.HttpSecurity#authorizeRequests() * @deprecated Use {@link AuthorizeHttpRequestsConfigurer} instead @@ -106,10 +107,9 @@ public final class ExpressionUrlAuthorizationConfigurer getExpressionHandler(H http) } ApplicationContext context = http.getSharedObject(ApplicationContext.class); if (context != null) { - String[] roleHiearchyBeanNames = context.getBeanNamesForType(RoleHierarchy.class); - if (roleHiearchyBeanNames.length == 1) { - defaultHandler.setRoleHierarchy(context.getBean(roleHiearchyBeanNames[0], RoleHierarchy.class)); - } - String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = context - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - defaultHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } - String[] permissionEvaluatorBeanNames = context.getBeanNamesForType(PermissionEvaluator.class); - if (permissionEvaluatorBeanNames.length == 1) { - PermissionEvaluator permissionEvaluator = context.getBean(permissionEvaluatorBeanNames[0], - PermissionEvaluator.class); - defaultHandler.setPermissionEvaluator(permissionEvaluator); - } + context.getBeanProvider(RoleHierarchy.class).ifUnique(defaultHandler::setRoleHierarchy); + context.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> defaultHandler + .setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix())); + context.getBeanProvider(PermissionEvaluator.class).ifUnique(defaultHandler::setPermissionEvaluator); } this.expressionHandler = postProcess(defaultHandler); return this.expressionHandler; @@ -249,6 +238,16 @@ public ExpressionInterceptUrlRegistry withObjectPostProcessor(ObjectPostProcesso return this; } + /** + * @deprecated + */ + @Deprecated(since = "6.4", forRemoval = true) + public ExpressionInterceptUrlRegistry withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return this; + } + public H and() { return ExpressionUrlAuthorizationConfigurer.this.and(); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index 87079485791..569dc8d1248 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -584,7 +584,7 @@ public FeaturePolicyConfig featurePolicy(String policyDirectives) { * * @return the {@link PermissionsPolicyConfig} for additional configuration * @since 5.5 - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} or + * @deprecated For removal in 7.0. Use {@link #permissionsPolicyHeader(Customizer)} or * {@code permissionsPolicy(Customizer.withDefaults())} to stick with defaults. See * the documentation @@ -610,14 +610,39 @@ public PermissionsPolicyConfig permissionsPolicy() { * @return the {@link PermissionsPolicyConfig} for additional configuration * @throws IllegalArgumentException if policyDirectives is {@code null} or empty * @since 5.5 + * @deprecated For removal in 7.0. Use {@link #permissionsPolicyHeader(Customizer)} + * instead * @see PermissionsPolicyHeaderWriter */ + @Deprecated(since = "6.4", forRemoval = true) public PermissionsPolicyConfig permissionsPolicy(Customizer permissionsPolicyCustomizer) { this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter(); permissionsPolicyCustomizer.customize(this.permissionsPolicy); return this.permissionsPolicy; } + /** + * Allows configuration for + * Permissions + * Policy. + *

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

    + * Configuration is provided to the {@link PermissionsPolicyHeaderWriter} which is + * responsible for writing the header. + * @return the {@link PermissionsPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + * @since 6.4 + * @see PermissionsPolicyHeaderWriter + */ + public HeadersConfigurer permissionsPolicyHeader( + Customizer permissionsPolicyCustomizer) { + this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter(); + permissionsPolicyCustomizer.customize(this.permissionsPolicy); + return this; + } + /** * Allows configuration for diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java index c968508a2db..51802672264 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.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. @@ -179,8 +179,7 @@ private void registerDefaults(B http) { allMatcher.setUseEquals(true); RequestMatcher notHtmlMatcher = new NegatedRequestMatcher( new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.TEXT_HTML)); - RequestMatcher restNotHtmlMatcher = new AndRequestMatcher( - Arrays.asList(notHtmlMatcher, restMatcher)); + RequestMatcher restNotHtmlMatcher = new AndRequestMatcher(Arrays.asList(notHtmlMatcher, restMatcher)); RequestMatcher preferredMatcher = new OrRequestMatcher( Arrays.asList(X_REQUESTED_WITH, restNotHtmlMatcher, allMatcher)); registerDefaultEntryPoint(http, preferredMatcher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java index 0125a22baed..83880336541 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,7 +18,6 @@ import java.util.UUID; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.RememberMeAuthenticationProvider; @@ -34,6 +33,7 @@ import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; @@ -78,6 +78,7 @@ * * @author Rob Winch * @author Eddú Meléndez + * @author Ngoc Nhan * @since 3.2 */ public final class RememberMeConfigurer> @@ -296,6 +297,13 @@ public void configure(H http) { rememberMeFilter.setSecurityContextRepository(securityContextRepository); } rememberMeFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); + + SessionAuthenticationStrategy sessionAuthenticationStrategy = http + .getSharedObject(SessionAuthenticationStrategy.class); + if (sessionAuthenticationStrategy != null) { + rememberMeFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy); + } + rememberMeFilter = postProcess(rememberMeFilter); http.addFilter(rememberMeFilter); } @@ -444,20 +452,12 @@ private C getSharedOrBean(H http, Class type) { if (shared != null) { return shared; } - return getBeanOrNull(type); - } - private T getBeanOrNull(Class type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + return context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java index e7eae3f2831..712c89073f8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.List; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -67,6 +66,7 @@ * * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 * @see RequestCache */ @@ -134,12 +134,8 @@ private T getBeanOrNull(Class type) { if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + + return context.getBeanProvider(type).getIfUnique(); } @SuppressWarnings("unchecked") diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java index ba4769d996b..a1b64f1ea0b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.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. @@ -56,6 +56,7 @@ * * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 */ public final class ServletApiConfigurer> @@ -92,12 +93,9 @@ public void configure(H http) { } ApplicationContext context = http.getSharedObject(ApplicationContext.class); if (context != null) { - String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = context - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.securityContextRequestFilter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } + context.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.securityContextRequestFilter + .setRolePrefix(grantedAuthorityDefaults.getRolePrefix())); this.securityContextRequestFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); } this.securityContextRequestFilter = postProcess(this.securityContextRequestFilter); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 504d68262ce..fc4a2a38804 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -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. @@ -25,7 +25,6 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.GenericApplicationListenerAdapter; @@ -100,6 +99,7 @@ * * @author Rob Winch * @author Onur Kagan Ozcan + * @author Ngoc Nhan * @since 3.2 * @see SessionManagementFilter * @see ConcurrentSessionFilter @@ -630,12 +630,8 @@ private T getBeanOrNull(Class type) { if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + + return context.getBeanProvider(type).getIfUnique(); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java index 7f8c8e9a41f..88e45080eff 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.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. @@ -28,7 +28,7 @@ import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.vote.AuthenticatedVoter; import org.springframework.security.access.vote.RoleVoter; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; @@ -117,6 +117,14 @@ public UrlAuthorizationConfigurer withObjectPostProcessor(ObjectPostProcessor return this; } + @Deprecated(since = "6.4", forRemoval = true) + @Override + public UrlAuthorizationConfigurer withObjectPostProcessor( + org.springframework.security.config.annotation.ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return this; + } + /** * Creates the default {@link AccessDecisionVoter} instances used if an * {@link AccessDecisionManager} was not specified. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java new file mode 100644 index 00000000000..1a955e523da --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity; +import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider; +import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository; +import org.springframework.security.web.webauthn.management.MapUserCredentialRepository; +import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository; +import org.springframework.security.web.webauthn.management.UserCredentialRepository; +import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; +import org.springframework.security.web.webauthn.management.Webauthn4JRelyingPartyOperations; +import org.springframework.security.web.webauthn.registration.DefaultWebAuthnRegistrationPageGeneratingFilter; +import org.springframework.security.web.webauthn.registration.PublicKeyCredentialCreationOptionsFilter; +import org.springframework.security.web.webauthn.registration.WebAuthnRegistrationFilter; + +/** + * Configures WebAuthn for Spring Security applications + * + * @param the type of builder + * @author Rob Winch + * @since 6.4 + */ +public class WebAuthnConfigurer> + extends AbstractHttpConfigurer, H> { + + private String rpId; + + private String rpName; + + private Set allowedOrigins = new HashSet<>(); + + private boolean disableDefaultRegistrationPage = false; + + /** + * The Relying Party id. + * @param rpId the relying party id + * @return the {@link WebAuthnConfigurer} for further customization + */ + public WebAuthnConfigurer rpId(String rpId) { + this.rpId = rpId; + return this; + } + + /** + * Sets the relying party name + * @param rpName the relying party name + * @return the {@link WebAuthnConfigurer} for further customization + */ + public WebAuthnConfigurer rpName(String rpName) { + this.rpName = rpName; + return this; + } + + /** + * Convenience method for {@link #allowedOrigins(Set)} + * @param allowedOrigins the allowed origins + * @return the {@link WebAuthnConfigurer} for further customization + * @see #allowedOrigins(Set) + */ + public WebAuthnConfigurer allowedOrigins(String... allowedOrigins) { + return allowedOrigins(Set.of(allowedOrigins)); + } + + /** + * Sets the allowed origins. + * @param allowedOrigins the allowed origins + * @return the {@link WebAuthnConfigurer} for further customization + * @see #allowedOrigins(String...) + */ + public WebAuthnConfigurer allowedOrigins(Set allowedOrigins) { + this.allowedOrigins = allowedOrigins; + return this; + } + + /** + * Configures whether the default webauthn registration should be disabled. Setting it + * to {@code true} will prevent the configurer from registering the + * {@link DefaultWebAuthnRegistrationPageGeneratingFilter}. + * @param disable disable the default registration page if true, enable it otherwise + * @return the {@link WebAuthnConfigurer} for further customization + */ + public WebAuthnConfigurer disableDefaultRegistrationPage(boolean disable) { + this.disableDefaultRegistrationPage = disable; + return this; + } + + @Override + public void configure(H http) throws Exception { + UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> { + throw new IllegalStateException("Missing UserDetailsService Bean"); + }); + PublicKeyCredentialUserEntityRepository userEntities = getSharedOrBean(http, + PublicKeyCredentialUserEntityRepository.class) + .orElse(userEntityRepository()); + UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class) + .orElse(userCredentialRepository()); + WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); + WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); + webAuthnAuthnFilter.setAuthenticationManager( + new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); + http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class); + http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class); + http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class); + http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class); + + DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http + .getSharedObject(DefaultLoginPageGeneratingFilter.class); + boolean isLoginPageEnabled = loginPageGeneratingFilter != null && loginPageGeneratingFilter.isEnabled(); + if (isLoginPageEnabled) { + loginPageGeneratingFilter.setPasskeysEnabled(true); + loginPageGeneratingFilter.setResolveHeaders((request) -> { + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + return Map.of(csrfToken.getHeaderName(), csrfToken.getToken()); + }); + } + + if (!this.disableDefaultRegistrationPage) { + http.addFilterAfter(new DefaultWebAuthnRegistrationPageGeneratingFilter(userEntities, userCredentials), + AuthorizationFilter.class); + if (!isLoginPageEnabled) { + http.addFilter(DefaultResourcesFilter.css()); + } + } + + if (isLoginPageEnabled || !this.disableDefaultRegistrationPage) { + http.addFilter(DefaultResourcesFilter.webauthn()); + } + } + + private Optional getSharedOrBean(H http, Class type) { + C shared = http.getSharedObject(type); + return Optional.ofNullable(shared).or(() -> getBeanOrNull(type)); + } + + private Optional getBeanOrNull(Class type) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return Optional.empty(); + } + try { + return Optional.of(context.getBean(type)); + } + catch (NoSuchBeanDefinitionException ex) { + return Optional.empty(); + } + } + + private MapUserCredentialRepository userCredentialRepository() { + return new MapUserCredentialRepository(); + } + + private PublicKeyCredentialUserEntityRepository userEntityRepository() { + return new MapPublicKeyCredentialUserEntityRepository(); + } + + private WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations( + PublicKeyCredentialUserEntityRepository userEntities, UserCredentialRepository userCredentials) { + Optional webauthnOperationsBean = getBeanOrNull( + WebAuthnRelyingPartyOperations.class); + if (webauthnOperationsBean.isPresent()) { + return webauthnOperationsBean.get(); + } + Webauthn4JRelyingPartyOperations result = new Webauthn4JRelyingPartyOperations(userEntities, userCredentials, + PublicKeyCredentialRpEntity.builder().id(this.rpId).name(this.rpName).build(), this.allowedOrigins); + return result; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index 7f89cdf184e..a3818e2a9ac 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,7 +18,6 @@ import jakarta.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; @@ -74,6 +73,7 @@ * * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 */ public final class X509Configurer> @@ -214,20 +214,11 @@ private C getSharedOrBean(H http, Class type) { if (shared != null) { return shared; } - return getBeanOrNull(type); - } - - private T getBeanOrNull(Class type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + return context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java deleted file mode 100644 index bbf32d09c92..00000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.util.function.Function; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtValidators; - -final class DefaultOidcLogoutTokenValidatorFactory implements Function> { - - @Override - public OAuth2TokenValidator apply(ClientRegistration clientRegistration) { - return JwtValidators.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration)); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index 3af7db6d02f..2aae05bbb9c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -86,6 +86,7 @@ * * @author Joe Grandja * @author Parikshit Dutta + * @author Ngoc Nhan * @since 5.1 * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2AuthorizationCodeGrantFilter @@ -290,7 +291,9 @@ private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { return this.authorizationRequestResolver; } ClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(getBuilder()); - return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, + ResolvableType resolvableType = ResolvableType.forClass(OAuth2AuthorizationRequestResolver.class); + OAuth2AuthorizationRequestResolver bean = getBeanOrNull(resolvableType); + return (bean != null) ? bean : new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); } @@ -335,13 +338,10 @@ private OAuth2AuthorizedClientRepository getAuthorizedClientRepository(B builder @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context != null) { - String[] names = context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) context.getBean(names[0]); - } + if (context == null) { + return null; } - return null; + return (T) context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java index 184042f8231..485c068e85b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -116,10 +116,17 @@ private static > OAuth2AuthorizedClientService static > OidcSessionRegistry getOidcSessionRegistry(B builder) { OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class); - if (sessionRegistry == null) { + if (sessionRegistry != null) { + return sessionRegistry; + } + ApplicationContext context = builder.getSharedObject(ApplicationContext.class); + if (context.getBeanNamesForType(OidcSessionRegistry.class).length == 1) { + sessionRegistry = context.getBean(OidcSessionRegistry.class); + } + else { sessionRegistry = new InMemoryOidcSessionRegistry(); - builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry); } + builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry); return sessionRegistry; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 913a8f1211e..16cf1c0e188 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -149,6 +149,7 @@ * * @author Joe Grandja * @author Kazuki Shimizu + * @author Ngoc Nhan * @since 5.0 * @see HttpSecurity#oauth2Login() * @see OAuth2AuthorizationRequestRedirectFilter @@ -460,12 +461,10 @@ private JwtDecoderFactory getJwtDecoderFactoryBean() { if (names.length > 1) { throw new NoUniqueBeanDefinitionException(type, names); } - if (names.length == 1) { - return (JwtDecoderFactory) this.getBuilder() - .getSharedObject(ApplicationContext.class) - .getBean(names[0]); - } - return null; + return (JwtDecoderFactory) this.getBuilder() + .getSharedObject(ApplicationContext.class) + .getBeanProvider(type) + .getIfUnique(); } private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() { @@ -517,15 +516,13 @@ private OAuth2UserService getOAuth2UserService() return (bean != null) ? bean : new DefaultOAuth2UserService(); } + @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context != null) { - String[] names = context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) context.getBean(names[0]); - } + if (context == null) { + return null; } - return null; + return (T) context.getBeanProvider(type).getIfUnique(); } private void initDefaultLoginFilter(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java index f65b1c11c09..827b0a05548 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.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. @@ -16,10 +16,12 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; +import java.io.Serial; import java.util.Collections; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; /** * An {@link org.springframework.security.core.Authentication} implementation that @@ -35,15 +37,21 @@ */ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken { + @Serial + private static final long serialVersionUID = 9095810699956350287L; + private final OidcLogoutToken logoutToken; + private final ClientRegistration clientRegistration; + /** * Construct an {@link OidcBackChannelLogoutAuthentication} * @param logoutToken a deserialized, verified OIDC Logout Token */ - OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) { + OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) { super(Collections.emptyList()); this.logoutToken = logoutToken; + this.clientRegistration = clientRegistration; setAuthenticated(true); } @@ -63,4 +71,8 @@ public OidcLogoutToken getCredentials() { return this.logoutToken; } + ClientRegistration getClientRegistration() { + return this.clientRegistration; + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java index 16731f19561..07debd74209 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.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. @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; +import java.util.function.Function; + import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JOSEObjectTypeVerifier; @@ -31,11 +33,12 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.BadJwtException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -64,7 +67,8 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio * Construct an {@link OidcBackChannelLogoutAuthenticationProvider} */ OidcBackChannelLogoutAuthenticationProvider() { - DefaultOidcLogoutTokenValidatorFactory jwtValidator = new DefaultOidcLogoutTokenValidatorFactory(); + Function> jwtValidator = (clientRegistration) -> JwtValidators + .createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration)); this.logoutTokenDecoderFactory = (clientRegistration) -> { String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri(); if (!StringUtils.hasText(jwkSetUri)) { @@ -81,8 +85,7 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio .jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(typeVerifier)) .build(); decoder.setJwtValidator(jwtValidator.apply(clientRegistration)); - decoder.setClaimSetConverter( - new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters())); + decoder.setClaimSetConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter()); return decoder; }; } @@ -101,7 +104,7 @@ public Authentication authenticate(Authentication authentication) throws Authent OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken) .claims((claims) -> claims.putAll(jwt.getClaims())) .build(); - return new OidcBackChannelLogoutAuthentication(oidcLogoutToken); + return new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java index 0a03ec83838..f76aeb8c30e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java @@ -58,7 +58,7 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter { private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter(); - private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); + private final LogoutHandler logoutHandler; /** * Construct an {@link OidcBackChannelLogoutFilter} @@ -68,11 +68,13 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter { * Logout Tokens */ OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter, - AuthenticationManager authenticationManager) { + AuthenticationManager authenticationManager, LogoutHandler logoutHandler) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + Assert.notNull(logoutHandler, "logoutHandler cannot be null"); this.authenticationConverter = authenticationConverter; this.authenticationManager = authenticationManager; + this.logoutHandler = logoutHandler; } /** @@ -126,14 +128,4 @@ private OAuth2Error oauth2Error(Exception ex) { "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); } - /** - * The strategy for expiring all Client sessions indicated by the logout request. - * Defaults to {@link OidcBackChannelLogoutHandler}. - * @param logoutHandler the {@link LogoutHandler} to use - */ - void setLogoutHandler(LogoutHandler logoutHandler) { - Assert.notNull(logoutHandler, "logoutHandler cannot be null"); - this.logoutHandler = logoutHandler; - } - } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java index 26ded200034..d7348f5c09d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java @@ -29,10 +29,9 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.core.OAuth2Error; @@ -40,6 +39,8 @@ import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; @@ -51,25 +52,29 @@ * Back-Channel Logout Token and invalidates each one. * * @author Josh Cummings - * @since 6.2 + * @since 6.4 * @see OIDC Back-Channel Logout * Spec */ -final class OidcBackChannelLogoutHandler implements LogoutHandler { +public final class OidcBackChannelLogoutHandler implements LogoutHandler { private final Log logger = LogFactory.getLog(getClass()); - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); + private final OidcSessionRegistry sessionRegistry; private RestOperations restOperations = new RestTemplate(); - private String logoutUri = "{baseScheme}://localhost{basePort}/logout"; + private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}"; private String sessionCookieName = "JSESSIONID"; private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter(); + public OidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry) { + this.sessionRegistry = sessionRegistry; + } + @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) { @@ -86,7 +91,7 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut for (OidcSessionInformation session : sessions) { totalCount++; try { - eachLogout(request, session); + eachLogout(request, token, session); invalidatedCount++; } catch (RestClientException ex) { @@ -103,18 +108,23 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut } } - private void eachLogout(HttpServletRequest request, OidcSessionInformation session) { + private void eachLogout(HttpServletRequest request, OidcBackChannelLogoutAuthentication token, + OidcSessionInformation session) { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId()); for (Map.Entry credential : session.getAuthorities().entrySet()) { headers.add(credential.getKey(), credential.getValue()); } - String logout = computeLogoutEndpoint(request); - HttpEntity entity = new HttpEntity<>(null, headers); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String logout = computeLogoutEndpoint(request, token); + MultiValueMap body = new LinkedMultiValueMap(); + body.add("logout_token", token.getPrincipal().getTokenValue()); + body.add("_spring_security_internal_logout", "true"); + HttpEntity entity = new HttpEntity<>(body, headers); this.restOperations.postForEntity(logout, entity, Object.class); } - String computeLogoutEndpoint(HttpServletRequest request) { + String computeLogoutEndpoint(HttpServletRequest request, OidcBackChannelLogoutAuthentication token) { // @formatter:off UriComponents uriComponents = UriComponentsBuilder .fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) @@ -137,6 +147,9 @@ String computeLogoutEndpoint(HttpServletRequest request) { int port = uriComponents.getPort(); uriVariables.put("basePort", (port == -1) ? "" : ":" + port); + String registrationId = token.getClientRegistration().getRegistrationId(); + uriVariables.put("registrationId", registrationId); + return UriComponentsBuilder.fromUriString(this.logoutUri) .buildAndExpand(uriVariables) .toUriString(); @@ -158,34 +171,13 @@ private void handleLogoutFailure(HttpServletResponse response, OAuth2Error error } } - /** - * Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that - * this class uses - * {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify - * sessions. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - /** - * Use this {@link RestOperations} to perform the per-session back-channel logout - * @param restOperations the {@link RestOperations} to use - */ - void setRestOperations(RestOperations restOperations) { - Assert.notNull(restOperations, "restOperations cannot be null"); - this.restOperations = restOperations; - } - /** * Use this logout URI for performing per-session logout. Defaults to {@code /logout} * since that is the default URI for * {@link org.springframework.security.web.authentication.logout.LogoutFilter}. * @param logoutUri the URI to use */ - void setLogoutUri(String logoutUri) { + public void setLogoutUri(String logoutUri) { Assert.hasText(logoutUri, "logoutUri cannot be empty"); this.logoutUri = logoutUri; } @@ -197,7 +189,7 @@ void setLogoutUri(String logoutUri) { * Note that if you are using Spring Session, this likely needs to change to SESSION. * @param sessionCookieName the cookie name to use */ - void setSessionCookieName(String sessionCookieName) { + public void setSessionCookieName(String sessionCookieName) { Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty"); this.sessionCookieName = sessionCookieName; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java index 4a227e3be8e..e609389e8fe 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.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. @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; +import java.io.Serial; + import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -29,6 +31,9 @@ */ class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { + @Serial + private static final long serialVersionUID = -1568528983223505540L; + private final String logoutToken; private final ClientRegistration clientRegistration; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java index 38c2e55e6b8..1095350dc5b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java @@ -19,16 +19,24 @@ import java.util.function.Consumer; import java.util.function.Function; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.logout.CompositeLogoutHandler; import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.util.Assert; @@ -57,6 +65,7 @@ * * * @author Josh Cummings + * @author Ngoc Nhan * @since 6.2 * @see HttpSecurity#oidcLogout() * @see OidcBackChannelLogoutFilter @@ -140,8 +149,11 @@ private AuthenticationManager authenticationManager() { } private LogoutHandler logoutHandler(B http) { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); - logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http)); + OidcBackChannelLogoutHandler logoutHandler = getBeanOrNull(OidcBackChannelLogoutHandler.class); + if (logoutHandler != null) { + return logoutHandler; + } + logoutHandler = new OidcBackChannelLogoutHandler(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http)); return logoutHandler; } @@ -176,21 +188,135 @@ private LogoutHandler logoutHandler(B http) { */ public BackChannelLogoutConfigurer logoutUri(String logoutUri) { this.logoutHandler = (http) -> { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); - logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http)); + OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler( + OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http)); logoutHandler.setLogoutUri(logoutUri); return logoutHandler; }; return this; } + /** + * Configure what and how per-session logout will be performed. + * + *

    + * This overrides any value given to {@link #logoutUri(String)} + * + *

    + * By default, the resulting {@link LogoutHandler} will {@code POST} the session + * cookie and OIDC logout token back to the original back-channel logout endpoint. + * + *

    + * Using this method changes the underlying default that {@code POST}s the session + * cookie and CSRF token to your application's {@code /logout} endpoint. As such, + * it is recommended to call this instead of accepting the {@code /logout} default + * as this does not require any special CSRF configuration, even if you don't + * require other changes. + * + *

    + * For example, configuring Back-Channel Logout in the following way: + * + *

    +		 * 	http
    +		 *     	.oidcLogout((oidc) -> oidc
    +		 *     		.backChannel((backChannel) -> backChannel
    +		 *     			.logoutHandler(new OidcBackChannelLogoutHandler())
    +		 *     		)
    +		 *     	);
    +		 * 
    + * + * will make so that the per-session logout invocation no longer requires special + * CSRF configurations. + * + *

    + * The default URI is + * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, which is simply + * an internal version of the same endpoint exposed to your Back-Channel services. + * You can use {@link OidcBackChannelLogoutHandler#setLogoutUri(String)} to alter + * the scheme, server name, or port in the {@code Host} header to accommodate how + * your application would address itself internally. + * + *

    + * For example, if the way your application would internally call itself is on a + * different scheme and port than incoming traffic, you can configure the endpoint + * in the following way: + * + *

    +		 * 	http
    +		 * 		.oidcLogout((oidc) -> oidc
    +		 * 			.backChannel((backChannel) -> backChannel
    +		 * 				.logoutHandler("http://localhost:9000/logout/connect/back-channel/{registrationId}")
    +		 * 			)
    +		 * 		);
    +		 * 
    + * + *

    + * You can also publish it as a {@code @Bean} as follows: + * + *

    +		 *	@Bean
    +		 *	OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
    +		 *  	OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry);
    +		 *  	logoutHandler.setSessionCookieName("SESSION");
    +		 *  	return logoutHandler;
    +		 *	}
    +		 * 
    + * + * to have the same effect. + * @param logoutHandler the {@link LogoutHandler} to use each individual session + * @return {@link BackChannelLogoutConfigurer} for further customizations + * @since 6.4 + */ + public BackChannelLogoutConfigurer logoutHandler(LogoutHandler logoutHandler) { + this.logoutHandler = (http) -> logoutHandler; + return this; + } + void configure(B http) { + LogoutHandler oidcLogout = this.logoutHandler.apply(http); + LogoutHandler sessionLogout = new SecurityContextLogoutHandler(); + LogoutConfigurer logout = http.getConfigurer(LogoutConfigurer.class); + if (logout != null) { + sessionLogout = new CompositeLogoutHandler(logout.getLogoutHandlers()); + } OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http), - authenticationManager()); - filter.setLogoutHandler(this.logoutHandler.apply(http)); + authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout)); http.addFilterBefore(filter, CsrfFilter.class); } + @SuppressWarnings("unchecked") + private T getBeanOrNull(Class clazz) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + return (T) context.getBeanProvider(clazz).getIfUnique(); + } + + private static final class EitherLogoutHandler implements LogoutHandler { + + private final LogoutHandler left; + + private final LogoutHandler right; + + EitherLogoutHandler(LogoutHandler left, LogoutHandler right) { + this.left = left; + this.right = right; + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + if (request.getParameter("_spring_security_internal_logout") == null) { + this.left.logout(request, response, authentication); + } + else { + this.right.logout(request, response, authentication); + } + } + + } + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 01411c927c7..31a8c265a04 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -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. @@ -326,8 +326,7 @@ private void registerDefaultEntryPoint(H http) { allMatcher.setUseEquals(true); RequestMatcher notHtmlMatcher = new NegatedRequestMatcher( new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.TEXT_HTML)); - RequestMatcher restNotHtmlMatcher = new AndRequestMatcher( - Arrays.asList(notHtmlMatcher, restMatcher)); + RequestMatcher restNotHtmlMatcher = new AndRequestMatcher(Arrays.asList(notHtmlMatcher, restMatcher)); RequestMatcher preferredMatcher = new OrRequestMatcher( Arrays.asList(this.requestMatcher, X_REQUESTED_WITH, restNotHtmlMatcher, allMatcher)); exceptionHandling.defaultAuthenticationEntryPointFor(this.authenticationEntryPoint, preferredMatcher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java new file mode 100644 index 00000000000..15718bf51b5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java @@ -0,0 +1,337 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.ott; + +import java.util.Collections; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService; +import org.springframework.security.authentication.ott.OneTimeToken; +import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider; +import org.springframework.security.authentication.ott.OneTimeTokenService; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationFilter; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter; +import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter; +import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +public final class OneTimeTokenLoginConfigurer> + extends AbstractHttpConfigurer, H> { + + private final ApplicationContext context; + + private OneTimeTokenService oneTimeTokenService; + + private AuthenticationConverter authenticationConverter = new OneTimeTokenAuthenticationConverter(); + + private AuthenticationFailureHandler authenticationFailureHandler; + + private AuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + + private String defaultSubmitPageUrl = "/login/ott"; + + private boolean submitPageEnabled = true; + + private String loginProcessingUrl = "/login/ott"; + + private String tokenGeneratingUrl = "/ott/generate"; + + private OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler; + + private AuthenticationProvider authenticationProvider; + + public OneTimeTokenLoginConfigurer(ApplicationContext context) { + this.context = context; + } + + @Override + public void init(H http) { + AuthenticationProvider authenticationProvider = getAuthenticationProvider(http); + http.authenticationProvider(postProcess(authenticationProvider)); + configureDefaultLoginPage(http); + } + + private void configureDefaultLoginPage(H http) { + DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http + .getSharedObject(DefaultLoginPageGeneratingFilter.class); + if (loginPageGeneratingFilter == null) { + return; + } + loginPageGeneratingFilter.setOneTimeTokenEnabled(true); + loginPageGeneratingFilter.setOneTimeTokenGenerationUrl(this.tokenGeneratingUrl); + if (this.authenticationFailureHandler == null + && StringUtils.hasText(loginPageGeneratingFilter.getLoginPageUrl())) { + this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler( + loginPageGeneratingFilter.getLoginPageUrl() + "?error"); + } + } + + @Override + public void configure(H http) { + configureSubmitPage(http); + configureOttGenerateFilter(http); + configureOttAuthenticationFilter(http); + } + + private void configureOttAuthenticationFilter(H http) { + AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); + AuthenticationFilter oneTimeTokenAuthenticationFilter = new AuthenticationFilter(authenticationManager, + this.authenticationConverter); + oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http)); + oneTimeTokenAuthenticationFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl)); + oneTimeTokenAuthenticationFilter.setFailureHandler(getAuthenticationFailureHandler()); + oneTimeTokenAuthenticationFilter.setSuccessHandler(this.authenticationSuccessHandler); + http.addFilter(postProcess(oneTimeTokenAuthenticationFilter)); + } + + private SecurityContextRepository getSecurityContextRepository(H http) { + SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); + if (securityContextRepository != null) { + return securityContextRepository; + } + return new HttpSessionSecurityContextRepository(); + } + + private void configureOttGenerateFilter(H http) { + GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http), + getOneTimeTokenGenerationSuccessHandler(http)); + generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl)); + http.addFilter(postProcess(generateFilter)); + http.addFilter(DefaultResourcesFilter.css()); + } + + private OneTimeTokenGenerationSuccessHandler getOneTimeTokenGenerationSuccessHandler(H http) { + if (this.oneTimeTokenGenerationSuccessHandler == null) { + this.oneTimeTokenGenerationSuccessHandler = getBeanOrNull(http, OneTimeTokenGenerationSuccessHandler.class); + } + if (this.oneTimeTokenGenerationSuccessHandler == null) { + throw new IllegalStateException(""" + A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin(). + Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL. + """); + } + return this.oneTimeTokenGenerationSuccessHandler; + } + + private void configureSubmitPage(H http) { + if (!this.submitPageEnabled) { + return; + } + DefaultOneTimeTokenSubmitPageGeneratingFilter submitPage = new DefaultOneTimeTokenSubmitPageGeneratingFilter(); + submitPage.setResolveHiddenInputs(this::hiddenInputs); + submitPage.setRequestMatcher(antMatcher(HttpMethod.GET, this.defaultSubmitPageUrl)); + submitPage.setLoginProcessingUrl(this.loginProcessingUrl); + http.addFilter(postProcess(submitPage)); + } + + private AuthenticationProvider getAuthenticationProvider(H http) { + if (this.authenticationProvider != null) { + return this.authenticationProvider; + } + UserDetailsService userDetailsService = getContext().getBean(UserDetailsService.class); + this.authenticationProvider = new OneTimeTokenAuthenticationProvider(getOneTimeTokenService(http), + userDetailsService); + return this.authenticationProvider; + } + + /** + * Specifies the {@link AuthenticationProvider} to use when authenticating the user. + * @param authenticationProvider + */ + public OneTimeTokenLoginConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) { + Assert.notNull(authenticationProvider, "authenticationProvider cannot be null"); + this.authenticationProvider = authenticationProvider; + return this; + } + + /** + * Specifies the URL that a One-Time Token generate request will be processed. + * Defaults to {@code /ott/generate}. + * @param tokenGeneratingUrl + */ + public OneTimeTokenLoginConfigurer tokenGeneratingUrl(String tokenGeneratingUrl) { + Assert.hasText(tokenGeneratingUrl, "tokenGeneratingUrl cannot be null or empty"); + this.tokenGeneratingUrl = tokenGeneratingUrl; + return this; + } + + /** + * Specifies strategy to be used to handle generated one-time tokens. + * @param oneTimeTokenGenerationSuccessHandler + */ + public OneTimeTokenLoginConfigurer tokenGenerationSuccessHandler( + OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) { + Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null"); + this.oneTimeTokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler; + return this; + } + + /** + * Specifies the URL to process the login request, defaults to {@code /login/ott}. + * Only POST requests are processed, for that reason make sure that you pass a valid + * CSRF token if CSRF protection is enabled. + * @param loginProcessingUrl + * @see org.springframework.security.config.annotation.web.builders.HttpSecurity#csrf(Customizer) + */ + public OneTimeTokenLoginConfigurer loginProcessingUrl(String loginProcessingUrl) { + Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty"); + this.loginProcessingUrl = loginProcessingUrl; + return this; + } + + /** + * Configures whether the default one-time token submit page should be shown. This + * will prevent the {@link DefaultOneTimeTokenSubmitPageGeneratingFilter} to be + * configured. + * @param show + */ + public OneTimeTokenLoginConfigurer showDefaultSubmitPage(boolean show) { + this.submitPageEnabled = show; + return this; + } + + /** + * Sets the URL that the default submit page will be generated. Defaults to + * {@code /login/ott}. If you don't want to generate the default submit page you + * should use {@link #showDefaultSubmitPage(boolean)}. Note that this method always + * invoke {@link #showDefaultSubmitPage(boolean)} passing {@code true}. + * @param submitPageUrl + */ + public OneTimeTokenLoginConfigurer defaultSubmitPageUrl(String submitPageUrl) { + Assert.hasText(submitPageUrl, "submitPageUrl cannot be null or empty"); + this.defaultSubmitPageUrl = submitPageUrl; + showDefaultSubmitPage(true); + return this; + } + + /** + * Configures the {@link OneTimeTokenService} used to generate and consume + * {@link OneTimeToken} + * @param oneTimeTokenService + */ + public OneTimeTokenLoginConfigurer tokenService(OneTimeTokenService oneTimeTokenService) { + Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null"); + this.oneTimeTokenService = oneTimeTokenService; + return this; + } + + /** + * Use this {@link AuthenticationConverter} when converting incoming requests to an + * {@link Authentication}. By default, the {@link OneTimeTokenAuthenticationConverter} + * is used. + * @param authenticationConverter the {@link AuthenticationConverter} to use + */ + public OneTimeTokenLoginConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + return this; + } + + /** + * Specifies the {@link AuthenticationFailureHandler} to use when authentication + * fails. The default is redirecting to "/login?error" using + * {@link SimpleUrlAuthenticationFailureHandler} + * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use + * when authentication fails. + */ + public OneTimeTokenLoginConfigurer authenticationFailureHandler( + AuthenticationFailureHandler authenticationFailureHandler) { + Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); + this.authenticationFailureHandler = authenticationFailureHandler; + return this; + } + + /** + * Specifies the {@link AuthenticationSuccessHandler} to be used. The default is + * {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties + * set. + * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler}. + */ + public OneTimeTokenLoginConfigurer authenticationSuccessHandler( + AuthenticationSuccessHandler authenticationSuccessHandler) { + Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null"); + this.authenticationSuccessHandler = authenticationSuccessHandler; + return this; + } + + private AuthenticationFailureHandler getAuthenticationFailureHandler() { + if (this.authenticationFailureHandler != null) { + return this.authenticationFailureHandler; + } + this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error"); + return this.authenticationFailureHandler; + } + + private OneTimeTokenService getOneTimeTokenService(H http) { + if (this.oneTimeTokenService != null) { + return this.oneTimeTokenService; + } + OneTimeTokenService bean = getBeanOrNull(http, OneTimeTokenService.class); + if (bean != null) { + this.oneTimeTokenService = bean; + } + else { + this.oneTimeTokenService = new InMemoryOneTimeTokenService(); + } + return this.oneTimeTokenService; + } + + private C getBeanOrNull(H http, Class clazz) { + ApplicationContext context = http.getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + + return context.getBeanProvider(clazz).getIfUnique(); + } + + private Map hiddenInputs(HttpServletRequest request) { + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + return (token != null) ? Collections.singletonMap(token.getParameterName(), token.getToken()) + : Collections.emptyMap(); + } + + public ApplicationContext getContext() { + return this.context; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 468afafad4e..b07b034d143 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -16,10 +16,14 @@ package org.springframework.security.config.annotation.web.configurers.saml2; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import jakarta.servlet.http.HttpServletRequest; +import org.opensaml.core.Version; + import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; @@ -31,14 +35,19 @@ import org.springframework.security.core.Authentication; import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.OpenSaml4AuthenticationTokenConverter; +import org.springframework.security.saml2.provider.service.web.OpenSaml5AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.OpenSamlAuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; import org.springframework.security.web.AuthenticationEntryPoint; @@ -50,6 +59,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.ParameterRequestMatcher; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatchers; @@ -109,9 +119,17 @@ public final class Saml2LoginConfigurer> extends AbstractAuthenticationFilterConfigurer, Saml2WebSsoAuthenticationFilter> { + private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5"); + private String loginPage; - private String authenticationRequestUri = Saml2AuthenticationRequestResolver.DEFAULT_AUTHENTICATION_REQUEST_URI; + private String authenticationRequestUri = "/saml2/authenticate"; + + private String[] authenticationRequestParams = { "registrationId={registrationId}" }; + + private RequestMatcher authenticationRequestMatcher = RequestMatchers.anyOf( + new AntPathRequestMatcher(Saml2AuthenticationRequestResolver.DEFAULT_AUTHENTICATION_REQUEST_URI), + new AntPathQueryRequestMatcher(this.authenticationRequestUri, this.authenticationRequestParams)); private Saml2AuthenticationRequestResolver authenticationRequestResolver; @@ -196,11 +214,32 @@ public Saml2LoginConfigurer authenticationRequestResolver( * Request * @return the {@link Saml2LoginConfigurer} for further configuration * @since 6.0 + * @deprecated Use {@link #authenticationRequestUriQuery} instead */ + @Deprecated public Saml2LoginConfigurer authenticationRequestUri(String authenticationRequestUri) { - Assert.state(authenticationRequestUri.contains("{registrationId}"), - "authenticationRequestUri must contain {registrationId} path variable"); - this.authenticationRequestUri = authenticationRequestUri; + return authenticationRequestUriQuery(authenticationRequestUri); + } + + /** + * Customize the URL that the SAML Authentication Request will be sent to. This method + * also supports query parameters like so:
    +	 * 	authenticationRequestUriQuery("/saml/authenticate?registrationId={registrationId}")
    +	 * 
    {@link RelyingPartyRegistrations} + * @param authenticationRequestUriQuery the URI and query to use for the SAML 2.0 + * Authentication Request + * @return the {@link Saml2LoginConfigurer} for further configuration + * @since 6.0 + */ + public Saml2LoginConfigurer authenticationRequestUriQuery(String authenticationRequestUriQuery) { + Assert.state(authenticationRequestUriQuery.contains("{registrationId}"), + "authenticationRequestUri must contain {registrationId} path variable or query value"); + String[] parts = authenticationRequestUriQuery.split("[?&]"); + this.authenticationRequestUri = parts[0]; + this.authenticationRequestParams = new String[parts.length - 1]; + System.arraycopy(parts, 1, this.authenticationRequestParams, 0, parts.length - 1); + this.authenticationRequestMatcher = new AntPathQueryRequestMatcher(this.authenticationRequestUri, + this.authenticationRequestParams); return this; } @@ -255,7 +294,7 @@ public void init(B http) throws Exception { } else { Map providerUrlMap = getIdentityProviderUrlMap(this.authenticationRequestUri, - this.relyingPartyRegistrationRepository); + this.authenticationRequestParams, this.relyingPartyRegistrationRepository); boolean singleProvider = providerUrlMap.size() == 1; if (singleProvider) { // Setup auto-redirect to provider login page @@ -334,11 +373,18 @@ private Saml2AuthenticationRequestResolver getAuthenticationRequestResolver(B ht if (bean != null) { return bean; } - OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver( - relyingPartyRegistrationRepository(http)); - openSaml4AuthenticationRequestResolver - .setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri)); - return openSaml4AuthenticationRequestResolver; + if (USE_OPENSAML_5) { + OpenSaml5AuthenticationRequestResolver openSamlAuthenticationRequestResolver = new OpenSaml5AuthenticationRequestResolver( + relyingPartyRegistrationRepository(http)); + openSamlAuthenticationRequestResolver.setRequestMatcher(this.authenticationRequestMatcher); + return openSamlAuthenticationRequestResolver; + } + else { + OpenSaml4AuthenticationRequestResolver openSamlAuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver( + relyingPartyRegistrationRepository(http)); + openSamlAuthenticationRequestResolver.setRequestMatcher(this.authenticationRequestMatcher); + return openSamlAuthenticationRequestResolver; + } } private AuthenticationConverter getAuthenticationConverter(B http) { @@ -350,20 +396,43 @@ private AuthenticationConverter getAuthenticationConverter(B http) { if (authenticationConverterBean == null) { authenticationConverterBean = getBeanOrNull(http, OpenSamlAuthenticationTokenConverter.class); } - if (authenticationConverterBean == null) { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter( + if (authenticationConverterBean != null) { + return authenticationConverterBean; + } + if (USE_OPENSAML_5) { + authenticationConverterBean = getBeanOrNull(http, OpenSaml5AuthenticationTokenConverter.class); + if (authenticationConverterBean != null) { + return authenticationConverterBean; + } + OpenSaml5AuthenticationTokenConverter converter = new OpenSaml5AuthenticationTokenConverter( this.relyingPartyRegistrationRepository); converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); converter.setRequestMatcher(this.loginProcessingUrl); return converter; } - return authenticationConverterBean; + authenticationConverterBean = getBeanOrNull(http, OpenSaml4AuthenticationTokenConverter.class); + if (authenticationConverterBean != null) { + return authenticationConverterBean; + } + OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter( + this.relyingPartyRegistrationRepository); + converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); + converter.setRequestMatcher(this.loginProcessingUrl); + return converter; } private void registerDefaultAuthenticationProvider(B http) { - OpenSaml4AuthenticationProvider provider = getBeanOrNull(http, OpenSaml4AuthenticationProvider.class); - if (provider == null) { - http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider())); + if (USE_OPENSAML_5) { + OpenSaml5AuthenticationProvider provider = getBeanOrNull(http, OpenSaml5AuthenticationProvider.class); + if (provider == null) { + http.authenticationProvider(postProcess(new OpenSaml5AuthenticationProvider())); + } + } + else { + OpenSaml4AuthenticationProvider provider = getBeanOrNull(http, OpenSaml4AuthenticationProvider.class); + if (provider == null) { + http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider())); + } } } @@ -382,20 +451,28 @@ private void initDefaultLoginFilter(B http) { return; } loginPageGeneratingFilter.setSaml2LoginEnabled(true); - loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName( - this.getIdentityProviderUrlMap(this.authenticationRequestUri, this.relyingPartyRegistrationRepository)); + loginPageGeneratingFilter + .setSaml2AuthenticationUrlToProviderName(this.getIdentityProviderUrlMap(this.authenticationRequestUri, + this.authenticationRequestParams, this.relyingPartyRegistrationRepository)); loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); } @SuppressWarnings("unchecked") - private Map getIdentityProviderUrlMap(String authRequestPrefixUrl, + private Map getIdentityProviderUrlMap(String authRequestPrefixUrl, String[] authRequestQueryParams, RelyingPartyRegistrationRepository idpRepo) { Map idps = new LinkedHashMap<>(); if (idpRepo instanceof Iterable) { Iterable repo = (Iterable) idpRepo; - repo.forEach((p) -> idps.put(authRequestPrefixUrl.replace("{registrationId}", p.getRegistrationId()), - p.getRegistrationId())); + StringBuilder authRequestQuery = new StringBuilder("?"); + for (String authRequestQueryParam : authRequestQueryParams) { + authRequestQuery.append(authRequestQueryParam + "&"); + } + authRequestQuery.deleteCharAt(authRequestQuery.length() - 1); + String authenticationRequestUriQuery = authRequestPrefixUrl + authRequestQuery; + repo.forEach( + (p) -> idps.put(authenticationRequestUriQuery.replace("{registrationId}", p.getRegistrationId()), + p.getRegistrationId())); } return idps; } @@ -423,12 +500,7 @@ private C getBeanOrNull(B http, Class clazz) { if (context == null) { return null; } - try { - return context.getBean(clazz); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + return context.getBeanProvider(clazz).getIfUnique(); } private void setSharedObject(B http, Class clazz, C object) { @@ -437,4 +509,35 @@ private void setSharedObject(B http, Class clazz, C object) { } } + static class AntPathQueryRequestMatcher implements RequestMatcher { + + private final RequestMatcher matcher; + + AntPathQueryRequestMatcher(String path, String... params) { + List matchers = new ArrayList<>(); + matchers.add(new AntPathRequestMatcher(path)); + for (String param : params) { + String[] parts = param.split("="); + if (parts.length == 1) { + matchers.add(new ParameterRequestMatcher(parts[0])); + } + else { + matchers.add(new ParameterRequestMatcher(parts[0], parts[1])); + } + } + this.matcher = new AndRequestMatcher(matchers); + } + + @Override + public boolean matches(HttpServletRequest request) { + return matcher(request).isMatch(); + } + + @Override + public MatchResult matcher(HttpServletRequest request) { + return this.matcher.matcher(request); + } + + } + } 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..92c7cef819f 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,10 +18,9 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.function.Predicate; import jakarta.servlet.http.HttpServletRequest; +import org.opensaml.core.Version; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; @@ -33,19 +32,25 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutResponseValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestValidatorParametersResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestValidatorParametersResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestValidatorParametersResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutResponseResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestValidatorParametersResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler; @@ -60,6 +65,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; /** @@ -101,12 +107,15 @@ * Uses {@link CsrfTokenRepository} to add the {@link CsrfLogoutHandler}. * * @author Josh Cummings + * @author Ngoc Nhan * @since 5.6 * @see Saml2LogoutConfigurer */ public final class Saml2LogoutConfigurer> extends AbstractHttpConfigurer, H> { + private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5"); + private ApplicationContext context; private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; @@ -251,16 +260,28 @@ private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter( RelyingPartyRegistrationRepository registrations) { LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations); - RequestMatcher requestMatcher = createLogoutRequestMatcher(); - OpenSamlLogoutRequestValidatorParametersResolver parameters = new OpenSamlLogoutRequestValidatorParametersResolver( - registrations); - parameters.setRequestMatcher(requestMatcher); - Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(parameters, + Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter( + createSaml2LogoutResponseParametersResolver(registrations), this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers); filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); return postProcess(filter); } + private Saml2LogoutRequestValidatorParametersResolver createSaml2LogoutResponseParametersResolver( + RelyingPartyRegistrationRepository registrations) { + RequestMatcher requestMatcher = createLogoutRequestMatcher(); + if (USE_OPENSAML_5) { + OpenSaml5LogoutRequestValidatorParametersResolver parameters = new OpenSaml5LogoutRequestValidatorParametersResolver( + registrations); + parameters.setRequestMatcher(requestMatcher); + return parameters; + } + OpenSaml4LogoutRequestValidatorParametersResolver parameters = new OpenSaml4LogoutRequestValidatorParametersResolver( + registrations); + parameters.setRequestMatcher(requestMatcher); + return parameters; + } + private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter( RelyingPartyRegistrationRepository registrations) { Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations, @@ -316,10 +337,7 @@ private C getBeanOrNull(Class clazz) { if (this.context == null) { return null; } - if (this.context.getBeanNamesForType(clazz).length == 0) { - return null; - } - return this.context.getBean(clazz); + return this.context.getBeanProvider(clazz).getIfAvailable(); } /** @@ -398,16 +416,22 @@ public Saml2LogoutConfigurer and() { } private Saml2LogoutRequestValidator logoutRequestValidator() { - if (this.logoutRequestValidator == null) { - return new OpenSamlLogoutRequestValidator(); + if (this.logoutRequestValidator != null) { + return this.logoutRequestValidator; + } + if (USE_OPENSAML_5) { + return new OpenSaml5LogoutRequestValidator(); } - return this.logoutRequestValidator; + return new OpenSaml4LogoutRequestValidator(); } private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) { if (this.logoutRequestResolver != null) { return this.logoutRequestResolver; } + if (USE_OPENSAML_5) { + return new OpenSaml5LogoutRequestResolver(registrations); + } return new OpenSaml4LogoutRequestResolver(registrations); } @@ -474,17 +498,23 @@ public Saml2LogoutConfigurer and() { } private Saml2LogoutResponseValidator logoutResponseValidator() { - if (this.logoutResponseValidator == null) { - return new OpenSamlLogoutResponseValidator(); + if (this.logoutResponseValidator != null) { + return this.logoutResponseValidator; + } + if (USE_OPENSAML_5) { + return new OpenSaml5LogoutResponseValidator(); } - return this.logoutResponseValidator; + return new OpenSaml4LogoutResponseValidator(); } private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) { - if (this.logoutResponseResolver == null) { - return new OpenSaml4LogoutResponseResolver(registrations); + if (this.logoutResponseResolver != null) { + return this.logoutResponseResolver; } - return this.logoutResponseResolver; + if (USE_OPENSAML_5) { + return new OpenSaml5LogoutResponseResolver(registrations); + } + return new OpenSaml4LogoutResponseResolver(registrations); } } @@ -508,23 +538,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/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java index de20083f1e9..349e3a66066 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.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,11 +18,14 @@ import java.util.function.Function; +import org.opensaml.core.Version; + import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; +import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver; +import org.springframework.security.saml2.provider.service.metadata.OpenSaml5MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; @@ -73,6 +76,8 @@ public class Saml2MetadataConfigurer> extends AbstractHttpConfigurer, H> { + private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5"); + private final ApplicationContext context; private Function metadataResponseResolver; @@ -103,8 +108,14 @@ public Saml2MetadataConfigurer(ApplicationContext context) { public Saml2MetadataConfigurer metadataUrl(String metadataUrl) { Assert.hasText(metadataUrl, "metadataUrl cannot be empty"); this.metadataResponseResolver = (registrations) -> { + if (USE_OPENSAML_5) { + RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver( + registrations, new OpenSaml5MetadataResolver()); + metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl)); + return metadata; + } RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations, - new OpenSamlMetadataResolver()); + new OpenSaml4MetadataResolver()); metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl)); return metadata; }; @@ -143,7 +154,10 @@ private Saml2MetadataResponseResolver createMetadataResponseResolver(H http) { return metadataResponseResolver; } RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); - return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver()); + if (USE_OPENSAML_5) { + return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml5MetadataResolver()); + } + return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); } private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { @@ -160,10 +174,7 @@ private C getBeanOrNull(Class clazz) { if (this.context == null) { return null; } - if (this.context.getBeanNamesForType(clazz).length == 0) { - return null; - } - return this.context.getBean(clazz); + return this.context.getBeanProvider(clazz).getIfAvailable(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java index 21ad642a070..f5f7a2ddc2d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java @@ -86,7 +86,7 @@ @Target(ElementType.TYPE) @Documented @Import({ ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class, - ReactiveOAuth2ClientImportSelector.class }) + ReactiveOAuth2ClientImportSelector.class, ReactiveObservationImportSelector.class }) public @interface EnableWebFluxSecurity { } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java index 07808221f82..7432a9c565e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -93,9 +92,16 @@ ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar( @Configuration(proxyBeanMethods = false) static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer { - private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager; - private ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + private final ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + + OAuth2ClientWebFluxSecurityConfiguration( + ObjectProvider authorizedClientManager, + ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { + this.authorizedClientManager = authorizedClientManager.getIfUnique(); + this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; + } @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { @@ -105,19 +111,6 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { } } - @Autowired(required = false) - void setAuthorizedClientManager(List authorizedClientManager) { - if (authorizedClientManager.size() == 1) { - this.authorizedClientManager = authorizedClientManager.get(0); - } - } - - @Autowired - void setAuthorizedClientManagerRegistrar( - ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { - this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; - } - private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() { if (this.authorizedClientManager != null) { return this.authorizedClientManager; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java new file mode 100644 index 00000000000..efb921c3897 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.server.ObservationWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; +import org.springframework.web.server.ServerWebExchange; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor reactiveAuthenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthenticationManager postProcess(ReactiveAuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationReactiveAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public WebFilterChainDecorator postProcess(WebFilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationWebFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java new file mode 100644 index 00000000000..5b4bdaebf0c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebFluxSecurity} to conditionally import observation configuration + * when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class ReactiveObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = ReactiveObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { ReactiveObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 69c6e75c512..90b8bb19dfa 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.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. @@ -16,7 +16,7 @@ package org.springframework.security.config.annotation.web.reactive; -import io.micrometer.observation.ObservationRegistry; +import java.util.Map; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -29,11 +29,12 @@ import org.springframework.context.annotation.Scope; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -66,7 +67,7 @@ class ServerHttpSecurityConfiguration { private ReactiveCompromisedPasswordChecker compromisedPasswordChecker; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor postProcessor = ObjectPostProcessor.identity(); @Autowired(required = false) private BeanFactory beanFactory; @@ -97,8 +98,12 @@ void setUserDetailsPasswordService(ReactiveUserDetailsPasswordService userDetail } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setAuthenticationManagerPostProcessor( + Map> postProcessors) { + if (postProcessors.size() == 1) { + this.postProcessor = postProcessors.values().iterator().next(); + } + this.postProcessor = postProcessors.get("reactiveAuthenticationManagerPostProcessor"); } @Autowired(required = false) @@ -108,34 +113,40 @@ void setCompromisedPasswordChecker(ReactiveCompromisedPasswordChecker compromise @Bean static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer( - ObjectProvider authenticationPrincipalArgumentResolver) { + ObjectProvider authenticationPrincipalArgumentResolver, + ObjectProvider currentSecurityContextArgumentResolvers) { return new WebFluxConfigurer() { @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject()); + configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject(), + currentSecurityContextArgumentResolvers.getObject()); } }; } @Bean - AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() { + AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver( + ObjectProvider templateDefaults) { AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver( this.adapterRegistry); if (this.beanFactory != null) { resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); } + templateDefaults.ifAvailable(resolver::setTemplateDefaults); return resolver; } @Bean - CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver() { + CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver( + ObjectProvider templateDefaults) { CurrentSecurityContextArgumentResolver resolver = new CurrentSecurityContextArgumentResolver( this.adapterRegistry); if (this.beanFactory != null) { resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); } + templateDefaults.ifAvailable(resolver::setTemplateDefaults); return resolver; } @@ -162,10 +173,7 @@ private ReactiveAuthenticationManager authenticationManager() { } manager.setUserDetailsPasswordService(this.userDetailsPasswordService); manager.setCompromisedPasswordChecker(this.compromisedPasswordChecker); - if (!this.observationRegistry.isNoop()) { - return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager); - } - return manager; + return this.postProcessor.postProcess(manager); } return null; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java index 72ea9a1a3a8..0ead8c280a6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -19,8 +19,6 @@ import java.util.Arrays; import java.util.List; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -28,12 +26,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; -import org.springframework.security.web.server.ObservationWebFilterChainDecorator; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.security.web.server.WebFilterChainProxy.DefaultWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler; import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall; import org.springframework.util.ClassUtils; @@ -60,7 +60,7 @@ class WebFluxSecurityConfiguration { private List securityWebFilterChains; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor postProcessor = ObjectPostProcessor.identity(); static { isOAuth2Present = ClassUtils.isPresent(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, @@ -76,8 +76,8 @@ void setSecurityWebFilterChains(List securityWebFilterCh } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setFilterChainPostProcessor(ObjectPostProcessor postProcessor) { + this.postProcessor = postProcessor; } @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME) @@ -85,9 +85,8 @@ void setObservationRegistry(ObservationRegistry observationRegistry) { WebFilterChainProxy springSecurityWebFilterChainFilter(ObjectProvider firewall, ObjectProvider rejectedHandler) { WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains()); - if (!this.observationRegistry.isNoop()) { - proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry)); - } + WebFilterChainDecorator decorator = this.postProcessor.postProcess(new DefaultWebFilterChainDecorator()); + proxy.setFilterChainDecorator(decorator); firewall.ifUnique(proxy::setFirewall); rejectedHandler.ifUnique(proxy::setExchangeRejectedHandler); return proxy; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java index 2d3d6c00607..cd2baed7335 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java @@ -35,7 +35,7 @@ import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.vote.AffirmativeBased; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java index 8b10c3bc23d..f67f14f3430 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java @@ -52,7 +52,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented -@Import(WebSocketMessageBrokerSecurityConfiguration.class) +@Import({ WebSocketMessageBrokerSecurityConfiguration.class, WebSocketObservationImportSelector.class }) public @interface EnableWebSocketSecurity { } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java index ca0d49d97fe..635341c8bc8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java @@ -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. @@ -20,8 +20,6 @@ import java.util.List; import java.util.Map; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -33,8 +31,9 @@ import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; @@ -78,10 +77,12 @@ final class WebSocketMessageBrokerSecurityConfiguration private AuthorizationManager> authorizationManager = ANY_MESSAGE_AUTHENTICATED; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor>> postProcessor = ObjectPostProcessor.identity(); private ApplicationContext context; + private AnnotationTemplateExpressionDefaults templateDefaults; + WebSocketMessageBrokerSecurityConfiguration(ApplicationContext context) { this.context = context; } @@ -90,6 +91,7 @@ final class WebSocketMessageBrokerSecurityConfiguration public void addArgumentResolvers(List argumentResolvers) { AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(); resolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + resolver.setTemplateDefaults(this.templateDefaults); argumentResolvers.add(resolver); } @@ -102,9 +104,7 @@ public void configureClientInboundChannel(ChannelRegistration registration) { } AuthorizationManager> manager = this.authorizationManager; - if (!this.observationRegistry.isNoop()) { - manager = new ObservationAuthorizationManager<>(this.observationRegistry, manager); - } + manager = this.postProcessor.postProcess(manager); AuthorizationChannelInterceptor interceptor = new AuthorizationChannelInterceptor(manager); interceptor.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context)); interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); @@ -124,8 +124,14 @@ void setAuthorizationManager(AuthorizationManager> authorizationManag } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setMessageAuthorizationManagerPostProcessor( + ObjectPostProcessor>> postProcessor) { + this.postProcessor = postProcessor; + } + + @Autowired(required = false) + void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.templateDefaults = templateDefaults; } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java new file mode 100644 index 00000000000..341df742381 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.messaging.Message; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class WebSocketObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor>> webSocketAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java new file mode 100644 index 00000000000..3eb4e0b4454 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.socket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebSocketSecurity} to conditionally import observation + * configuration when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class WebSocketObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = WebSocketObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { WebSocketObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java index 9e2b6a8a65d..337fc081f79 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -26,7 +26,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.ObservationAuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -40,6 +39,7 @@ * has forgotten to declare the <authentication-manager> element. * * @author Luke Taylor + * @author Ngoc Nhan * @since 3.0 */ public class AuthenticationManagerFactoryBean implements FactoryBean, BeanFactoryAware { @@ -61,18 +61,18 @@ public AuthenticationManager getObject() throws Exception { if (!BeanIds.AUTHENTICATION_MANAGER.equals(ex.getBeanName())) { throw ex; } - UserDetailsService uds = getBeanOrNull(UserDetailsService.class); + UserDetailsService uds = this.bf.getBeanProvider(UserDetailsService.class).getIfUnique(); if (uds == null) { throw new NoSuchBeanDefinitionException(BeanIds.AUTHENTICATION_MANAGER, MISSING_BEAN_ERROR_MESSAGE); } DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(uds); - PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + PasswordEncoder passwordEncoder = this.bf.getBeanProvider(PasswordEncoder.class).getIfUnique(); if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); } provider.afterPropertiesSet(); - ProviderManager manager = new ProviderManager(Arrays.asList(provider)); + ProviderManager manager = new ProviderManager(Arrays.asList(provider)); if (this.observationRegistry.isNoop()) { return manager; } @@ -99,13 +99,4 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; } - private T getBeanOrNull(Class type) { - try { - return this.bf.getBean(type); - } - catch (NoSuchBeanDefinitionException noUds) { - return null; - } - } - } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 3172d407601..f997e5fa5e5 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -60,6 +60,7 @@ import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CsrfToken; @@ -155,6 +156,8 @@ final class AuthenticationConfigBuilder { private BeanDefinition logoutPageGenerationFilter; + private BeanDefinition defaultResourcesFilter; + private BeanDefinition etf; private final BeanReference requestCache; @@ -635,6 +638,9 @@ void createLoginPageFilterIfNeeded() { } this.loginPageGenerationFilter = loginPageFilter.getBeanDefinition(); this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition(); + this.defaultResourcesFilter = BeanDefinitionBuilder.rootBeanDefinition(DefaultResourcesFilter.class) + .setFactoryMethod("css") + .getBeanDefinition(); } } @@ -890,6 +896,9 @@ List getFilters() { filters.add(new OrderDecorator(this.loginPageGenerationFilter, SecurityFilters.LOGIN_PAGE_FILTER)); filters.add(new OrderDecorator(this.logoutPageGenerationFilter, SecurityFilters.LOGOUT_PAGE_FILTER)); } + if (this.defaultResourcesFilter != null) { + filters.add(new OrderDecorator(this.defaultResourcesFilter, SecurityFilters.DEFAULT_RESOURCES_FILTER)); + } if (this.basicFilter != null) { filters.add(new OrderDecorator(this.basicFilter, SecurityFilters.BASIC_AUTH_FILTER)); } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java index 48967d47cb2..53ff1238b0e 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthorizationFilterParser.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. @@ -76,12 +76,13 @@ class AuthorizationFilterParser implements BeanDefinitionParser { public BeanDefinition parse(Element element, ParserContext parserContext) { if (!isUseExpressions(element)) { parserContext.getReaderContext() - .error("AuthorizationManager must be used with `use-expressions=\"true\"", element); + .error("AuthorizationManager must be used with `use-expressions=\"true\"; either add `use-authorization-manager=\"false\"` or `use-expressions=`\"false\"` in your `` block", + element); return null; } if (StringUtils.hasText(element.getAttribute(ATT_ACCESS_DECISION_MANAGER_REF))) { parserContext.getReaderContext() - .error("AuthorizationManager cannot be used in conjunction with `access-decision-manager-ref`", + .error("AuthorizationManager cannot be used in conjunction with `access-decision-manager-ref`; either remove the reference to AccessDecisionManager or add `use-authorization-manager=\"false\"` to your `` block", element); return null; } diff --git a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java index 1e89e78cb8a..ce7c50be584 100644 --- a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.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. @@ -32,8 +32,8 @@ import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; @@ -221,8 +221,8 @@ private boolean checkLoginPageIsPublic(List filters, FilterInvocation lo AuthorizationManager authorizationManager = authorizationFilter .getAuthorizationManager(); try { - AuthorizationDecision decision = authorizationManager.check(() -> TEST, loginRequest.getHttpRequest()); - return decision != null && decision.isGranted(); + AuthorizationResult result = authorizationManager.authorize(() -> TEST, loginRequest.getHttpRequest()); + return result != null && result.isGranted(); } catch (Exception ex) { return false; @@ -252,8 +252,8 @@ private Supplier deriveAnonymousCheck(List filters, FilterInvoc return () -> { AuthorizationManager authorizationManager = authorizationFilter .getAuthorizationManager(); - AuthorizationDecision decision = authorizationManager.check(() -> token, loginRequest.getHttpRequest()); - return decision != null && decision.isGranted(); + AuthorizationResult result = authorizationManager.authorize(() -> token, loginRequest.getHttpRequest()); + return result != null && result.isGranted(); }; } return () -> true; diff --git a/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java b/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java index 611e46cbfad..570edfd764f 100644 --- a/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java +++ b/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-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. @@ -25,6 +25,7 @@ /** * @author Rob Winch + * @author Ngoc Nhan * @since 4.2 */ final class GrantedAuthorityDefaultsParserUtils { @@ -49,13 +50,8 @@ abstract static class AbstractGrantedAuthorityDefaultsBeanFactory implements App @Override public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.rolePrefix = grantedAuthorityDefaults.getRolePrefix()); } abstract Object getBean(); diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java index 530aba60f87..17d5469d059 100644 --- a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java @@ -16,6 +16,7 @@ package org.springframework.security.config.http; +import org.opensaml.core.Version; import org.w3c.dom.Element; import org.springframework.beans.BeanMetadataElement; @@ -23,10 +24,14 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver; import org.springframework.util.StringUtils; /** @@ -35,6 +40,8 @@ */ final class Saml2LoginBeanDefinitionParserUtils { + private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5"); + private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref"; private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref"; @@ -78,16 +85,21 @@ static BeanMetadataElement createDefaultAuthenticationRequestResolver( .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class) .addConstructorArgValue(relyingPartyRegistrationRepository) .getBeanDefinition(); - return BeanDefinitionBuilder.rootBeanDefinition( - "org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver") + if (USE_OPENSAML_5) { + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5AuthenticationRequestResolver.class) + .addConstructorArgValue(defaultRelyingPartyRegistrationResolver) + .getBeanDefinition(); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4AuthenticationRequestResolver.class) .addConstructorArgValue(defaultRelyingPartyRegistrationResolver) .getBeanDefinition(); } static BeanDefinition createAuthenticationProvider() { - return BeanDefinitionBuilder.rootBeanDefinition( - "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider") - .getBeanDefinition(); + if (USE_OPENSAML_5) { + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5AuthenticationProvider.class).getBeanDefinition(); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4AuthenticationProvider.class).getBeanDefinition(); } static BeanMetadataElement getAuthenticationConverter(Element element) { 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 545f0f06c67..24566458e11 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; @@ -229,23 +228,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/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java index c7cb1792d59..647add7e2c0 100644 --- a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java @@ -16,15 +16,22 @@ package org.springframework.security.config.http; +import org.opensaml.core.Version; import org.w3c.dom.Element; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml5LogoutResponseValidator; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml5LogoutResponseResolver; import org.springframework.util.StringUtils; /** @@ -33,6 +40,8 @@ */ final class Saml2LogoutBeanDefinitionParserUtils { + private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5"); + private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref"; private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref"; @@ -62,8 +71,12 @@ static BeanMetadataElement getLogoutResponseResolver(Element element, BeanMetada if (StringUtils.hasText(logoutResponseResolver)) { return new RuntimeBeanReference(logoutResponseResolver); } - return BeanDefinitionBuilder.rootBeanDefinition( - "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver") + if (USE_OPENSAML_5) { + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutResponseResolver.class) + .addConstructorArgValue(registrations) + .getBeanDefinition(); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutResponseResolver.class) .addConstructorArgValue(registrations) .getBeanDefinition(); } @@ -73,7 +86,10 @@ static BeanMetadataElement getLogoutRequestValidator(Element element) { if (StringUtils.hasText(logoutRequestValidator)) { return new RuntimeBeanReference(logoutRequestValidator); } - return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutRequestValidator.class).getBeanDefinition(); + if (USE_OPENSAML_5) { + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutRequestValidator.class).getBeanDefinition(); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutRequestValidator.class).getBeanDefinition(); } static BeanMetadataElement getLogoutResponseValidator(Element element) { @@ -81,7 +97,10 @@ static BeanMetadataElement getLogoutResponseValidator(Element element) { if (StringUtils.hasText(logoutResponseValidator)) { return new RuntimeBeanReference(logoutResponseValidator); } - return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutResponseValidator.class).getBeanDefinition(); + if (USE_OPENSAML_5) { + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutResponseValidator.class).getBeanDefinition(); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutResponseValidator.class).getBeanDefinition(); } static BeanMetadataElement getLogoutRequestRepository(Element element) { @@ -97,8 +116,12 @@ static BeanMetadataElement getLogoutRequestResolver(Element element, BeanMetadat if (StringUtils.hasText(logoutRequestResolver)) { return new RuntimeBeanReference(logoutRequestResolver); } - return BeanDefinitionBuilder.rootBeanDefinition( - "org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver") + if (USE_OPENSAML_5) { + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml5LogoutRequestResolver.class) + .addConstructorArgValue(registrations) + .getBeanDefinition(); + } + return BeanDefinitionBuilder.rootBeanDefinition(OpenSaml4LogoutRequestResolver.class) .addConstructorArgValue(registrations) .getBeanDefinition(); } diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index fc59835e347..0a399228b45 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -71,6 +71,8 @@ enum SecurityFilters { FORM_LOGIN_FILTER, + DEFAULT_RESOURCES_FILTER, + LOGIN_PAGE_FILTER, LOGOUT_PAGE_FILTER, diff --git a/config/src/main/java/org/springframework/security/config/http/UserDetailsServiceFactoryBean.java b/config/src/main/java/org/springframework/security/config/http/UserDetailsServiceFactoryBean.java index 227adbc0eca..00e349dc52c 100644 --- a/config/src/main/java/org/springframework/security/config/http/UserDetailsServiceFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/http/UserDetailsServiceFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -101,10 +101,10 @@ else if (bean instanceof UserDetailsService) { */ private UserDetailsService getUserDetailsService() { Map beans = getBeansOfType(CachingUserDetailsService.class); - if (beans.size() == 0) { + if (beans.isEmpty()) { beans = getBeansOfType(UserDetailsService.class); } - if (beans.size() == 0) { + if (beans.isEmpty()) { throw new ApplicationContextException("No UserDetailsService registered."); } if (beans.size() > 1) { @@ -124,7 +124,7 @@ public void setApplicationContext(ApplicationContext beanFactory) throws BeansEx // Check ancestor bean factories if they exist and the current one has none of the // required type BeanFactory parent = this.beanFactory.getParentBeanFactory(); - while (parent != null && beans.size() == 0) { + while (parent != null && beans.isEmpty()) { if (parent instanceof ListableBeanFactory) { beans = ((ListableBeanFactory) parent).getBeansOfType(type); } diff --git a/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.java index 70bb1965799..a2717d5be27 100644 --- a/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.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. @@ -88,6 +88,7 @@ * @author Ben Alex * @author Luke Taylor * @author Rob Winch + * @author Ngoc Nhan * @since 2.0 * @deprecated Use {@link MethodSecurityBeanDefinitionParser} instead */ @@ -483,13 +484,8 @@ abstract static class AbstractGrantedAuthorityDefaultsBeanFactory implements App @Override public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.rolePrefix = grantedAuthorityDefaults.getRolePrefix()); } } diff --git a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java index 8bde3921433..fef5a1a3549 100644 --- a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.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. @@ -67,6 +67,7 @@ * Processes the top-level "method-security" element. * * @author Josh Cummings + * @author Ngoc Nhan * @since 5.6 */ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser { @@ -307,13 +308,9 @@ public Class getObjectType() { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.expressionHandler + .setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix())); } } @@ -347,13 +344,9 @@ public Class getObjectType() { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.manager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.manager + .setRolePrefix(grantedAuthorityDefaults.getRolePrefix())); } public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { diff --git a/config/src/main/java/org/springframework/security/config/method/PointcutDelegatingAuthorizationManager.java b/config/src/main/java/org/springframework/security/config/method/PointcutDelegatingAuthorizationManager.java index d6aa4767f8f..1a47fc455ab 100644 --- a/config/src/main/java/org/springframework/security/config/method/PointcutDelegatingAuthorizationManager.java +++ b/config/src/main/java/org/springframework/security/config/method/PointcutDelegatingAuthorizationManager.java @@ -25,6 +25,7 @@ import org.springframework.aop.support.AopUtils; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; class PointcutDelegatingAuthorizationManager implements AuthorizationManager { @@ -37,11 +38,24 @@ class PointcutDelegatingAuthorizationManager implements AuthorizationManager authentication, MethodInvocation object) { + AuthorizationResult result = authorize(authentication, object); + if (result == null) { + return null; + } + if (result instanceof AuthorizationDecision decision) { + return decision; + } + throw new IllegalArgumentException( + "Please either call authorize or ensure that the returned result is of type AuthorizationDecision"); + } + + @Override + public AuthorizationResult authorize(Supplier authentication, MethodInvocation object) { for (Map.Entry> entry : this.managers.entrySet()) { Class targetClass = (object.getThis() != null) ? AopUtils.getTargetClass(object.getThis()) : null; if (entry.getKey().getClassFilter().matches(targetClass) && entry.getKey().getMethodMatcher().matches(object.getMethod(), targetClass)) { - return entry.getValue().check(authentication, object); + return entry.getValue().authorize(authentication, object); } } return new AuthorizationDecision(false); diff --git a/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java b/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java new file mode 100644 index 00000000000..d37d4218703 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.observation; + +import io.micrometer.observation.ObservationPredicate; + +/** + * An {@link ObservationPredicate} that can be used to change which Spring Security + * observations are made with Micrometer. + * + *

    + * By default, web requests are not observed and authentications and authorizations are + * observed. + * + * @author Josh Cummings + * @since 6.4 + */ +public final class SecurityObservationSettings { + + private final boolean observeRequests; + + private final boolean observeAuthentications; + + private final boolean observeAuthorizations; + + private SecurityObservationSettings(boolean observeRequests, boolean observeAuthentications, + boolean observeAuthorizations) { + this.observeRequests = observeRequests; + this.observeAuthentications = observeAuthentications; + this.observeAuthorizations = observeAuthorizations; + } + + /** + * Make no Spring Security observations + * @return a {@link SecurityObservationSettings} with all exclusions turned on + */ + public static SecurityObservationSettings noObservations() { + return new SecurityObservationSettings(false, false, false); + } + + /** + * Begin the configuration of a {@link SecurityObservationSettings} + * @return a {@link Builder} where filter chain observations are off and authn/authz + * observations are on + */ + public static Builder withDefaults() { + return new Builder(false, true, true); + } + + public boolean shouldObserveRequests() { + return this.observeRequests; + } + + public boolean shouldObserveAuthentications() { + return this.observeAuthentications; + } + + public boolean shouldObserveAuthorizations() { + return this.observeAuthorizations; + } + + /** + * A builder for configuring a {@link SecurityObservationSettings} + */ + public static final class Builder { + + private boolean observeRequests; + + private boolean observeAuthentications; + + private boolean observeAuthorizations; + + Builder(boolean observeRequests, boolean observeAuthentications, boolean observeAuthorizations) { + this.observeRequests = observeRequests; + this.observeAuthentications = observeAuthentications; + this.observeAuthorizations = observeAuthorizations; + } + + public Builder shouldObserveRequests(boolean excludeFilters) { + this.observeRequests = excludeFilters; + return this; + } + + public Builder shouldObserveAuthentications(boolean excludeAuthentications) { + this.observeAuthentications = excludeAuthentications; + return this; + } + + public Builder shouldObserveAuthorizations(boolean excludeAuthorizations) { + this.observeAuthorizations = excludeAuthorizations; + return this; + } + + public SecurityObservationSettings build() { + return new SecurityObservationSettings(this.observeRequests, this.observeAuthentications, + this.observeAuthorizations); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java index 60fe23c43ec..a8a10122bcf 100644 --- a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java @@ -39,6 +39,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.security.converter.RsaKeyConverters; import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; @@ -153,7 +154,7 @@ private static Map> getAssertingParties(Element elem } private static void addVerificationCredentials(Map assertingParty, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + AssertingPartyMetadata.Builder builder) { List verificationCertificateLocations = (List) assertingParty.get(ELT_VERIFICATION_CREDENTIAL); List verificationCredentials = new ArrayList<>(); for (String certificateLocation : verificationCertificateLocations) { @@ -163,7 +164,7 @@ private static void addVerificationCredentials(Map assertingPart } private static void addEncryptionCredentials(Map assertingParty, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + AssertingPartyMetadata.Builder builder) { List encryptionCertificateLocations = (List) assertingParty.get(ELT_ENCRYPTION_CREDENTIAL); List encryptionCredentials = new ArrayList<>(); for (String certificateLocation : encryptionCertificateLocations) { @@ -212,31 +213,33 @@ private List getRelyingPartyRegistrations(Element elem private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIfPossible( Element relyingPartyRegistrationElt, Map> assertingParties, ParserContext parserContext) { - String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID); - String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION); + String registrationId = resolveAttribute(parserContext, + relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID)); + String metadataLocation = resolveAttribute(parserContext, + relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION)); RelyingPartyRegistration.Builder builder; if (StringUtils.hasText(metadataLocation)) { builder = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId); } else { builder = RelyingPartyRegistration.withRegistrationId(registrationId) - .assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties, - apBuilder, parserContext)); + .assertingPartyMetadata((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt, + assertingParties, apBuilder, parserContext)); } - addRemainingProperties(relyingPartyRegistrationElt, builder); + addRemainingProperties(parserContext, relyingPartyRegistrationElt, builder); return builder; } - private static void addRemainingProperties(Element relyingPartyRegistrationElt, + private static void addRemainingProperties(ParserContext pc, Element relyingPartyRegistrationElt, RelyingPartyRegistration.Builder builder) { - String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID); - String singleLogoutServiceLocation = relyingPartyRegistrationElt - .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION); - String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt - .getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION); + String entityId = resolveAttribute(pc, relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID)); + String singleLogoutServiceLocation = resolveAttribute(pc, + relyingPartyRegistrationElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION)); + String singleLogoutServiceResponseLocation = resolveAttribute(pc, + relyingPartyRegistrationElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION)); Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt); - String assertionConsumerServiceLocation = relyingPartyRegistrationElt - .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION); + String assertionConsumerServiceLocation = resolveAttribute(pc, + relyingPartyRegistrationElt.getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION)); Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding( relyingPartyRegistrationElt); if (StringUtils.hasText(entityId)) { @@ -260,7 +263,7 @@ private static void addRemainingProperties(Element relyingPartyRegistrationElt, } private static void buildAssertingParty(Element relyingPartyElt, Map> assertingParties, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder, ParserContext parserContext) { + AssertingPartyMetadata.Builder builder, ParserContext parserContext) { String assertingPartyId = relyingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID); if (!assertingParties.containsKey(assertingPartyId)) { Object source = parserContext.extractSource(relyingPartyElt); @@ -293,7 +296,7 @@ private static void buildAssertingParty(Element relyingPartyElt, Map assertingParty, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + AssertingPartyMetadata.Builder builder) { String signingAlgorithmsAttr = getAsString(assertingParty, ATT_SIGNING_ALGORITHMS); if (StringUtils.hasText(signingAlgorithmsAttr)) { List signingAlgorithms = Arrays.asList(signingAlgorithmsAttr.split(",")); @@ -399,4 +402,8 @@ private static X509Certificate readCertificate(String certificateLocation) { } } + private static String resolveAttribute(ParserContext pc, String value) { + return pc.getReaderContext().getEnvironment().resolvePlaceholders(value); + } + } diff --git a/config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java b/config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java deleted file mode 100644 index 23d8502c800..00000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.util.function.Function; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtValidators; - -final class DefaultOidcLogoutTokenValidatorFactory implements Function> { - - @Override - public OAuth2TokenValidator apply(ClientRegistration clientRegistration) { - return JwtValidators.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration)); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java index c68063b6142..d5d95f471c1 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.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. @@ -16,10 +16,12 @@ package org.springframework.security.config.web.server; +import java.io.Serial; import java.util.Collections; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; /** * An {@link org.springframework.security.core.Authentication} implementation that @@ -35,15 +37,21 @@ */ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken { + @Serial + private static final long serialVersionUID = 9095810699956350287L; + private final OidcLogoutToken logoutToken; + private final ClientRegistration clientRegistration; + /** * Construct an {@link OidcBackChannelLogoutAuthentication} * @param logoutToken a deserialized, verified OIDC Logout Token */ - OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) { + OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) { super(Collections.emptyList()); this.logoutToken = logoutToken; + this.clientRegistration = clientRegistration; setAuthenticated(true); } @@ -63,4 +71,8 @@ public OidcLogoutToken getCredentials() { return this.logoutToken; } + ClientRegistration getClientRegistration() { + return this.clientRegistration; + } + } diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java index 6706c65c9cd..04334be1bfd 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java @@ -16,6 +16,8 @@ package org.springframework.security.config.web.server; +import java.util.function.Function; + import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JOSEObjectTypeVerifier; @@ -33,11 +35,13 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; import org.springframework.security.oauth2.jwt.BadJwtException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory; @@ -68,7 +72,8 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti * Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager} */ OidcBackChannelLogoutReactiveAuthenticationManager() { - DefaultOidcLogoutTokenValidatorFactory jwtValidator = new DefaultOidcLogoutTokenValidatorFactory(); + Function> jwtValidator = (clientRegistration) -> JwtValidators + .createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration)); this.logoutTokenDecoderFactory = (clientRegistration) -> { String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri(); if (!StringUtils.hasText(jwkSetUri)) { @@ -105,7 +110,7 @@ public Mono authenticate(Authentication authentication) throws A .map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken) .claims((claims) -> claims.putAll(jwt.getClaims())) .build()) - .map(OidcBackChannelLogoutAuthentication::new); + .map((oidcLogoutToken) -> new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration)); } private Mono decode(ClientRegistration registration, String token) { diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java index 8f1788c498f..1922f90defe 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java @@ -35,7 +35,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; @@ -61,7 +60,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter { private final ReactiveAuthenticationManager authenticationManager; - private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); + private final ServerLogoutHandler logoutHandler; private final HttpMessageWriter errorHttpMessageConverter = new EncoderHttpMessageWriter<>( new OAuth2ErrorEncoder()); @@ -74,11 +73,13 @@ class OidcBackChannelLogoutWebFilter implements WebFilter { * Logout Tokens */ OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter, - ReactiveAuthenticationManager authenticationManager) { + ReactiveAuthenticationManager authenticationManager, ServerLogoutHandler logoutHandler) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + Assert.notNull(logoutHandler, "logoutHandler cannot be null"); this.authenticationConverter = authenticationConverter; this.authenticationManager = authenticationManager; + this.logoutHandler = logoutHandler; } @Override @@ -121,14 +122,4 @@ private OAuth2Error oauth2Error(Exception ex) { "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); } - /** - * The strategy for expiring all Client sessions indicated by the logout request. - * Defaults to {@link OidcBackChannelServerLogoutHandler}. - * @param logoutHandler the {@link LogoutHandler} to use - */ - void setLogoutHandler(ServerLogoutHandler logoutHandler) { - Assert.notNull(logoutHandler, "logoutHandler cannot be null"); - this.logoutHandler = logoutHandler; - } - } diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java index c0c1e73bc61..e31eb766d9d 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java @@ -35,15 +35,15 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponents; @@ -54,26 +54,30 @@ * Back-Channel Logout Token and invalidates each one. * * @author Josh Cummings - * @since 6.2 + * @since 6.4 * @see OIDC Back-Channel Logout * Spec */ -final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler { +public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler { private final Log logger = LogFactory.getLog(getClass()); - private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry(); + private final ReactiveOidcSessionRegistry sessionRegistry; private final HttpMessageWriter errorHttpMessageConverter = new EncoderHttpMessageWriter<>( new OAuth2ErrorEncoder()); private WebClient web = WebClient.create(); - private String logoutUri = "{baseScheme}://localhost{basePort}/logout"; + private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}"; private String sessionCookieName = "SESSION"; + public OidcBackChannelServerLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) { + this.sessionRegistry = sessionRegistry; + } + @Override public Mono logout(WebFilterExchange exchange, Authentication authentication) { if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) { @@ -89,7 +93,7 @@ public Mono logout(WebFilterExchange exchange, Authentication authenticati AtomicInteger invalidatedCount = new AtomicInteger(0); return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> { totalCount.incrementAndGet(); - return eachLogout(exchange, session).flatMap((response) -> { + return eachLogout(exchange, session, token).flatMap((response) -> { invalidatedCount.incrementAndGet(); return Mono.empty(); }).onErrorResume((ex) -> { @@ -110,17 +114,26 @@ public Mono logout(WebFilterExchange exchange, Authentication authenticati }); } - private Mono> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) { + private Mono> eachLogout(WebFilterExchange exchange, OidcSessionInformation session, + OidcBackChannelLogoutAuthentication token) { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId()); for (Map.Entry credential : session.getAuthorities().entrySet()) { headers.add(credential.getKey(), credential.getValue()); } - String logout = computeLogoutEndpoint(exchange.getExchange().getRequest()); - return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity(); + String logout = computeLogoutEndpoint(exchange.getExchange().getRequest(), token); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("logout_token", token.getPrincipal().getTokenValue()); + body.add("_spring_security_internal_logout", "true"); + return this.web.post() + .uri(logout) + .headers((h) -> h.putAll(headers)) + .body(BodyInserters.fromFormData(body)) + .retrieve() + .toBodilessEntity(); } - String computeLogoutEndpoint(ServerHttpRequest request) { + String computeLogoutEndpoint(ServerHttpRequest request, OidcBackChannelLogoutAuthentication token) { // @formatter:off UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()) .replacePath(request.getPath().contextPath().value()) @@ -142,6 +155,9 @@ String computeLogoutEndpoint(ServerHttpRequest request) { int port = uriComponents.getPort(); uriVariables.put("basePort", (port == -1) ? "" : ":" + port); + String registrationId = token.getClientRegistration().getRegistrationId(); + uriVariables.put("registrationId", registrationId); + return UriComponentsBuilder.fromUriString(this.logoutUri) .buildAndExpand(uriVariables) .toUriString(); @@ -160,34 +176,13 @@ private Mono handleLogoutFailure(ServerWebExchange exchange, OAuth2Error e exchange.getResponse(), Collections.emptyMap()); } - /** - * Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that - * this class uses - * {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify - * sessions. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - /** - * Use this {@link WebClient} to perform the per-session back-channel logout - * @param web the {@link WebClient} to use - */ - void setWebClient(WebClient web) { - Assert.notNull(web, "web cannot be null"); - this.web = web; - } - /** * Use this logout URI for performing per-session logout. Defaults to {@code /logout} * since that is the default URI for * {@link org.springframework.security.web.authentication.logout.LogoutFilter}. * @param logoutUri the URI to use */ - void setLogoutUri(String logoutUri) { + public void setLogoutUri(String logoutUri) { Assert.hasText(logoutUri, "logoutUri cannot be empty"); this.logoutUri = logoutUri; } @@ -199,7 +194,7 @@ void setLogoutUri(String logoutUri) { * Note that if you are using Spring Session, this likely needs to change to SESSION. * @param sessionCookieName the cookie name to use */ - void setSessionCookieName(String sessionCookieName) { + public void setSessionCookieName(String sessionCookieName) { Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty"); this.sessionCookieName = sessionCookieName; } diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java b/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java index 8d5ab818a5f..617f2969804 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.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. @@ -16,6 +16,8 @@ package org.springframework.security.config.web.server; +import java.io.Serial; + import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -29,6 +31,9 @@ */ class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { + @Serial + private static final long serialVersionUID = -1568528983223505540L; + private final String logoutToken; private final ClientRegistration clientRegistration; diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java index 101d5b15c52..c32b78ad231 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java +++ b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -67,6 +67,16 @@ public enum SecurityWebFiltersOrder { LOGOUT_PAGE_GENERATING, + /** + * {@link org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter} + */ + ONE_TIME_TOKEN, + + /** + * {@link org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter} + */ + ONE_TIME_TOKEN_SUBMIT_PAGE_GENERATING, + /** * {@link org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter} */ diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 3820b29d140..ac72b75eb9a 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; @@ -33,13 +34,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import reactor.util.context.Context; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -52,13 +53,16 @@ import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.authentication.ott.OneTimeToken; +import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService; +import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager; +import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService; import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; -import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; @@ -152,6 +156,9 @@ import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter; +import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenAuthenticationConverter; +import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler; import org.springframework.security.web.server.authorization.AuthorizationContext; import org.springframework.security.web.server.authorization.AuthorizationWebFilter; import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager; @@ -194,8 +201,10 @@ import org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter; import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; import org.springframework.security.web.server.transport.HttpsRedirectWebFilter; +import org.springframework.security.web.server.ui.DefaultResourcesWebFilter; import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter; import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter; +import org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter; import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; @@ -206,6 +215,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.CorsProcessor; import org.springframework.web.cors.reactive.CorsWebFilter; @@ -346,6 +356,8 @@ public class ServerHttpSecurity { private AnonymousSpec anonymous; + private OneTimeTokenLoginSpec oneTimeTokenLogin; + protected ServerHttpSecurity() { } @@ -1547,6 +1559,43 @@ public ServerHttpSecurity authenticationManager(ReactiveAuthenticationManager ma return this; } + /** + * Configures One-Time Token Login Support. + * + *

    Example Configuration

    + * + *
    +	 * @Configuration
    +	 * @EnableWebFluxSecurity
    +	 * public class SecurityConfig {
    +	 *
    +	 * 	@Bean
    +	 * 	public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception {
    +	 * 		http
    +	 * 			// ...
    +	 * 			.oneTimeTokenLogin(Customizer.withDefaults());
    +	 * 		return http.build();
    +	 * 	}
    +	 *
    +	 * 	@Bean
    +	 * 	public ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
    +	 * 		return new MyMagicLinkServerOneTimeTokenGenerationSuccessHandler();
    +	 * 	}
    +	 *
    +	 * }
    +	 * 
    + * @param oneTimeTokenLoginCustomizer the {@link Customizer} to provide more options + * for the {@link OneTimeTokenLoginSpec} + * @return the {@link ServerHttpSecurity} for further customizations + */ + public ServerHttpSecurity oneTimeTokenLogin(Customizer oneTimeTokenLoginCustomizer) { + if (this.oneTimeTokenLogin == null) { + this.oneTimeTokenLogin = new OneTimeTokenLoginSpec(); + } + oneTimeTokenLoginCustomizer.customize(this.oneTimeTokenLogin); + return this; + } + /** * Builds the {@link SecurityWebFilterChain} * @return the {@link SecurityWebFilterChain} @@ -1639,6 +1688,18 @@ else if (this.securityContextRepository != null) { this.logout.configure(this); } this.requestCache.configure(this); + if (this.oneTimeTokenLogin != null) { + if (this.oneTimeTokenLogin.securityContextRepository != null) { + this.oneTimeTokenLogin.securityContextRepository(this.oneTimeTokenLogin.securityContextRepository); + } + else if (this.securityContextRepository != null) { + this.oneTimeTokenLogin.securityContextRepository(this.securityContextRepository); + } + else { + this.oneTimeTokenLogin.securityContextRepository(new WebSessionServerSecurityContextRepository()); + } + this.oneTimeTokenLogin.configure(this); + } this.addFilterAt(new SecurityContextServerWebExchangeWebFilter(), SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE); if (this.authorizeExchange != null) { @@ -1733,26 +1794,34 @@ private T getBean(Class beanClass) { } private T getBeanOrDefault(Class beanClass, T defaultInstance) { - T bean = getBeanOrNull(beanClass); - if (bean == null) { + if (this.context == null) { return defaultInstance; } - return bean; + return this.context.getBeanProvider(beanClass).getIfUnique(() -> defaultInstance); + } + + private ObjectProvider getBeanProvider(ResolvableType type) { + if (this.context == null) { + return new ObjectProvider<>() { + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + }; + } + return this.context.getBeanProvider(type); } private T getBeanOrNull(Class beanClass) { return getBeanOrNull(ResolvableType.forClass(beanClass)); } + @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { if (this.context == null) { return null; } - String[] names = this.context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) this.context.getBean(names[0]); - } - return null; + return (T) this.context.getBeanProvider(type).getIfUnique(); } private T getBeanOrNull(String beanName, Class requiredClass) { @@ -1798,6 +1867,17 @@ public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegis private PathPatternParser pathPatternParser; + private ObjectPostProcessor> postProcessor = ObjectPostProcessor + .identity(); + + public AuthorizeExchangeSpec() { + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + ResolvableType.forClassWithGenerics(ReactiveAuthorizationManager.class, ServerWebExchange.class)); + ObjectProvider>> postProcessor = getBeanProvider( + type); + postProcessor.ifUnique((p) -> this.postProcessor = p); + } + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -1850,10 +1930,7 @@ protected void configure(ServerHttpSecurity http) { Assert.state(this.matcher == null, () -> "The matcher " + this.matcher + " does not have an access rule defined"); ReactiveAuthorizationManager manager = this.managerBldr.build(); - ObservationRegistry registry = getBeanOrDefault(ObservationRegistry.class, ObservationRegistry.NOOP); - if (!registry.isNoop()) { - manager = new ObservationReactiveAuthorizationManager<>(registry, manager); - } + manager = this.postProcessor.postProcess(manager); AuthorizationWebFilter result = new AuthorizationWebFilter(manager); http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION); } @@ -2957,7 +3034,8 @@ protected void configure(ServerHttpSecurity http) { if (http.authenticationEntryPoint != null) { return; } - if (http.formLogin != null && http.formLogin.isEntryPointExplicit) { + if (http.formLogin != null && http.formLogin.isEntryPointExplicit + || http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)) { return; } LoginPageGeneratingWebFilter loginPage = null; @@ -2974,6 +3052,7 @@ protected void configure(ServerHttpSecurity http) { } if (loginPage != null) { http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + http.addFilterBefore(DefaultResourcesWebFilter.css(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); if (http.logout != null) { http.addFilterAt(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING); @@ -4133,6 +4212,8 @@ public final class OAuth2LoginSpec { private ServerAuthenticationFailureHandler authenticationFailureHandler; + private String loginPage; + private OAuth2LoginSpec() { } @@ -4362,6 +4443,19 @@ private ServerWebExchangeMatcher getAuthenticationMatcher() { return this.authenticationMatcher; } + /** + * Specifies the URL to send users to if login is required. A default login page + * will be generated when this attribute is not specified. + * @param loginPage the URL to send users to if login is required + * @return the {@link OAuth2LoginSpec} for further configuration + * @since 6.4 + */ + public OAuth2LoginSpec loginPage(String loginPage) { + Assert.hasText(loginPage, "loginPage cannot be empty"); + this.loginPage = loginPage; + return this; + } + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -4408,12 +4502,6 @@ protected void configure(ServerHttpSecurity http) { } private void setDefaultEntryPoints(ServerHttpSecurity http) { - String defaultLoginPage = "/login"; - Map urlToText = http.oauth2Login.getLinks(); - String providerLoginPage = null; - if (urlToText.size() == 1) { - providerLoginPage = urlToText.keySet().iterator().next(); - } MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN); @@ -4427,22 +4515,34 @@ MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTM ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher); ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher, htmlMatcher); - if (providerLoginPage != null) { - ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher( - defaultLoginPage); - ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico"); - ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher( - new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); - - ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher, - new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher)); - RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint( - providerLoginPage); - entryPoint.setRequestCache(http.requestCache.requestCache); - http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint)); + String loginPage = "/login"; + if (StringUtils.hasText(this.loginPage)) { + loginPage = this.loginPage; + } + else { + Map urlToText = http.oauth2Login.getLinks(); + String providerLoginPage = null; + if (urlToText.size() == 1) { + providerLoginPage = urlToText.keySet().iterator().next(); + } + if (providerLoginPage != null) { + ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher( + loginPage); + ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher( + "/favicon.ico"); + ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher( + new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); + + ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher, + new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher)); + RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint( + providerLoginPage); + entryPoint.setRequestCache(http.requestCache.requestCache); + http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint)); + } } RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint( - defaultLoginPage); + loginPage); defaultEntryPoint.setRequestCache(http.requestCache.requestCache); http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint)); } @@ -4486,7 +4586,9 @@ private ReactiveOAuth2UserService getOidcUserService( if (bean != null) { return bean; } - return new OidcReactiveOAuth2UserService(); + OidcReactiveOAuth2UserService reactiveOAuth2UserService = new OidcReactiveOAuth2UserService(); + reactiveOAuth2UserService.setOauth2UserService(getOauth2UserService()); + return reactiveOAuth2UserService; } private ReactiveOAuth2UserService getOauth2UserService() { @@ -4532,9 +4634,12 @@ private ReactiveClientRegistrationRepository getClientRegistrationRepository() { } private OAuth2AuthorizationRequestRedirectWebFilter getRedirectWebFilter() { - OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter; - if (this.authorizationRequestResolver != null) { - return new OAuth2AuthorizationRequestRedirectWebFilter(this.authorizationRequestResolver); + ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver = this.authorizationRequestResolver; + if (authorizationRequestResolver == null) { + authorizationRequestResolver = getBeanOrNull(ServerOAuth2AuthorizationRequestResolver.class); + } + if (authorizationRequestResolver != null) { + return new OAuth2AuthorizationRequestRedirectWebFilter(authorizationRequestResolver); } return new OAuth2AuthorizationRequestRedirectWebFilter(getClientRegistrationRepository()); } @@ -4786,11 +4891,22 @@ public OAuth2ClientSpec authenticationManager(ReactiveAuthenticationManager auth private ReactiveAuthenticationManager getAuthenticationManager() { if (this.authenticationManager == null) { this.authenticationManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager( - new WebClientReactiveAuthorizationCodeTokenResponseClient()); + getAuthorizationCodeTokenResponseClient()); } return this.authenticationManager; } + private ReactiveOAuth2AccessTokenResponseClient getAuthorizationCodeTokenResponseClient() { + ResolvableType resolvableType = ResolvableType.forClassWithGenerics( + ReactiveOAuth2AccessTokenResponseClient.class, OAuth2AuthorizationCodeGrantRequest.class); + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOrNull( + resolvableType); + if (accessTokenResponseClient == null) { + accessTokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient(); + } + return accessTokenResponseClient; + } + /** * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look * the value up as a Bean. @@ -5469,7 +5585,7 @@ private ReactiveClientRegistrationRepository getClientRegistrationRepository() { private ReactiveOidcSessionRegistry getSessionRegistry() { if (this.sessionRegistry == null && ServerHttpSecurity.this.oauth2Login == null) { - return new InMemoryReactiveOidcSessionRegistry(); + return getBeanOrDefault(ReactiveOidcSessionRegistry.class, new InMemoryReactiveOidcSessionRegistry()); } if (this.sessionRegistry == null) { return ServerHttpSecurity.this.oauth2Login.oidcSessionRegistry; @@ -5501,8 +5617,12 @@ private ReactiveAuthenticationManager authenticationManager() { } private ServerLogoutHandler logoutHandler() { - OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); - logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry()); + OidcBackChannelServerLogoutHandler logoutHandler = getBeanOrNull( + OidcBackChannelServerLogoutHandler.class); + if (logoutHandler != null) { + return logoutHandler; + } + logoutHandler = new OidcBackChannelServerLogoutHandler(OidcLogoutSpec.this.getSessionRegistry()); return logoutHandler; } @@ -5520,9 +5640,9 @@ private ServerLogoutHandler logoutHandler() { * *

    * By default, the URI is set to - * {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme - * and port of the original back-channel request is preserved, while the host - * and endpoint are changed. + * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, meaning + * that the scheme and port of the original back-channel request is preserved, + * while the host and endpoint are changed. * *

    * If you are using Spring Security for the logout endpoint, the path part of @@ -5533,27 +5653,135 @@ private ServerLogoutHandler logoutHandler() { * that the scheme, server name, or port in the {@code Host} header are * different from how you would address the same server internally. * @param logoutUri the URI to request logout on the back-channel - * @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for - * further customizations + * @return the {@link BackChannelLogoutConfigurer} for further customizations * @since 6.2.4 */ public BackChannelLogoutConfigurer logoutUri(String logoutUri) { this.logoutHandler = () -> { - OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); - logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry()); + OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler( + OidcLogoutSpec.this.getSessionRegistry()); logoutHandler.setLogoutUri(logoutUri); return logoutHandler; }; return this; } + /** + * Configure what and how per-session logout will be performed. + * + *

    + * This overrides any value given to {@link #logoutUri(String)} + * + *

    + * By default, the resulting {@link LogoutHandler} will {@code POST} the + * session cookie and OIDC logout token back to the original back-channel + * logout endpoint. + * + *

    + * Using this method changes the underlying default that {@code POST}s the + * session cookie and CSRF token to your application's {@code /logout} + * endpoint. As such, it is recommended to call this instead of accepting the + * {@code /logout} default as this does not require any special CSRF + * configuration, even if you don't require other changes. + * + *

    + * For example, configuring Back-Channel Logout in the following way: + * + *

    +			 * 	http
    +			 *     	.oidcLogout((oidc) -> oidc
    +			 *     		.backChannel((backChannel) -> backChannel
    +			 *     			.logoutHandler(new OidcBackChannelServerLogoutHandler())
    +			 *     		)
    +			 *     	);
    +			 * 
    + * + * will make so that the per-session logout invocation no longer requires + * special CSRF configurations. + * + *

    + * The default URI is + * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, which is + * simply an internal version of the same endpoint exposed to your + * Back-Channel services. You can use + * {@link OidcBackChannelServerLogoutHandler#setLogoutUri(String)} to alter + * the scheme, server name, or port in the {@code Host} header to accommodate + * how your application would address itself internally. + * + *

    + * For example, if the way your application would internally call itself is on + * a different scheme and port than incoming traffic, you can configure the + * endpoint in the following way: + * + *

    +			 * 	http
    +			 * 		.oidcLogout((oidc) -> oidc
    +			 * 			.backChannel((backChannel) -> backChannel
    +			 * 				.logoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}")
    +			 * 			)
    +			 * 		);
    +			 * 
    + * + *

    + * You can also publish it as a {@code @Bean} as follows: + * + *

    +			 *	@Bean
    +			 *	OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
    +			 *  	OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
    +			 *  	logoutHandler.setLogoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}");
    +			 *  	return logoutHandler;
    +			 *	}
    +			 * 
    + * + * to have the same effect. + * @param logoutHandler the {@link ServerLogoutHandler} to use each individual + * session + * @return {@link BackChannelLogoutConfigurer} for further customizations + * @since 6.4 + */ + public BackChannelLogoutConfigurer logoutHandler(ServerLogoutHandler logoutHandler) { + this.logoutHandler = () -> logoutHandler; + return this; + } + void configure(ServerHttpSecurity http) { + ServerLogoutHandler oidcLogout = this.logoutHandler.get(); + ServerLogoutHandler sessionLogout = new SecurityContextServerLogoutHandler(); + LogoutSpec logout = ServerHttpSecurity.this.logout; + if (logout != null) { + sessionLogout = new DelegatingServerLogoutHandler(logout.logoutHandlers); + } OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(), - authenticationManager()); - filter.setLogoutHandler(this.logoutHandler.get()); + authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout)); http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF); } + private static final class EitherLogoutHandler implements ServerLogoutHandler { + + private final ServerLogoutHandler left; + + private final ServerLogoutHandler right; + + EitherLogoutHandler(ServerLogoutHandler left, ServerLogoutHandler right) { + this.left = left; + this.right = right; + } + + @Override + public Mono logout(WebFilterExchange exchange, Authentication authentication) { + return exchange.getExchange().getFormData().flatMap((data) -> { + if (data.getFirst("_spring_security_internal_logout") == null) { + return this.left.logout(exchange, authentication); + } + else { + return this.right.logout(exchange, authentication); + } + }); + } + + } + } } @@ -5683,4 +5911,295 @@ private AnonymousSpec() { } + /** + * Configures One-Time Token Login Support + * + * @author Max Batischev + * @since 6.4 + * @see #oneTimeTokenLogin(Customizer) + */ + public final class OneTimeTokenLoginSpec { + + private ReactiveAuthenticationManager authenticationManager; + + private ReactiveOneTimeTokenService tokenService; + + private ServerAuthenticationConverter authenticationConverter = new ServerOneTimeTokenAuthenticationConverter(); + + private ServerAuthenticationFailureHandler authenticationFailureHandler; + + private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler( + "/"); + + private final List defaultSuccessHandlers = new ArrayList<>( + List.of(this.defaultSuccessHandler)); + + private final List authenticationSuccessHandlers = new ArrayList<>(); + + private ServerOneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler; + + private ServerSecurityContextRepository securityContextRepository; + + private String loginProcessingUrl = "/login/ott"; + + private String defaultSubmitPageUrl = "/login/ott"; + + private String tokenGeneratingUrl = "/ott/generate"; + + private boolean submitPageEnabled = true; + + protected void configure(ServerHttpSecurity http) { + configureSubmitPage(http); + configureOttGenerateFilter(http); + configureOttAuthenticationFilter(http); + configureDefaultLoginPage(http); + } + + private void configureOttAuthenticationFilter(ServerHttpSecurity http) { + AuthenticationWebFilter ottWebFilter = new AuthenticationWebFilter(getAuthenticationManager()); + ottWebFilter.setServerAuthenticationConverter(this.authenticationConverter); + ottWebFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler()); + ottWebFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler()); + ottWebFilter.setRequiresAuthenticationMatcher( + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.loginProcessingUrl)); + ottWebFilter.setSecurityContextRepository(this.securityContextRepository); + http.addFilterAt(ottWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); + } + + private void configureSubmitPage(ServerHttpSecurity http) { + if (!this.submitPageEnabled) { + return; + } + OneTimeTokenSubmitPageGeneratingWebFilter submitPage = new OneTimeTokenSubmitPageGeneratingWebFilter(); + submitPage.setLoginProcessingUrl(this.loginProcessingUrl); + + if (StringUtils.hasText(this.defaultSubmitPageUrl)) { + submitPage.setRequestMatcher( + ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, this.defaultSubmitPageUrl)); + } + http.addFilterAt(submitPage, SecurityWebFiltersOrder.ONE_TIME_TOKEN_SUBMIT_PAGE_GENERATING); + } + + private void configureOttGenerateFilter(ServerHttpSecurity http) { + GenerateOneTimeTokenWebFilter generateFilter = new GenerateOneTimeTokenWebFilter(getTokenService(), + getTokenGenerationSuccessHandler()); + generateFilter + .setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl)); + http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN); + } + + private void configureDefaultLoginPage(ServerHttpSecurity http) { + if (http.formLogin != null) { + for (WebFilter webFilter : http.webFilters) { + OrderedWebFilter orderedWebFilter = (OrderedWebFilter) webFilter; + if (orderedWebFilter.webFilter instanceof LoginPageGeneratingWebFilter loginPageGeneratingFilter) { + loginPageGeneratingFilter.setOneTimeTokenEnabled(true); + loginPageGeneratingFilter.setGenerateOneTimeTokenUrl(this.tokenGeneratingUrl); + break; + } + } + } + } + + /** + * Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The + * default list contains a {@link RedirectServerAuthenticationSuccessHandler} that + * redirects to "/". + * @param handlersConsumer the handlers consumer + * @return the {@link OneTimeTokenLoginSpec} to continue configuring + */ + public OneTimeTokenLoginSpec authenticationSuccessHandler( + Consumer> handlersConsumer) { + Assert.notNull(handlersConsumer, "handlersConsumer cannot be null"); + handlersConsumer.accept(this.authenticationSuccessHandlers); + return this; + } + + /** + * Specifies the {@link ServerAuthenticationSuccessHandler} + * @param authenticationSuccessHandler the + * {@link ServerAuthenticationSuccessHandler}. + */ + public OneTimeTokenLoginSpec authenticationSuccessHandler( + ServerAuthenticationSuccessHandler authenticationSuccessHandler) { + Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null"); + authenticationSuccessHandler((handlers) -> { + handlers.clear(); + handlers.add(authenticationSuccessHandler); + }); + return this; + } + + private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler() { + if (this.authenticationSuccessHandlers.isEmpty()) { + return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers); + } + return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers); + } + + /** + * Specifies the {@link ServerAuthenticationFailureHandler} to use when + * authentication fails. The default is redirecting to "/login?error" using + * {@link RedirectServerAuthenticationFailureHandler} + * @param authenticationFailureHandler the + * {@link ServerAuthenticationFailureHandler} to use when authentication fails. + */ + public OneTimeTokenLoginSpec authenticationFailureHandler( + ServerAuthenticationFailureHandler authenticationFailureHandler) { + Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); + this.authenticationFailureHandler = authenticationFailureHandler; + return this; + } + + ServerAuthenticationFailureHandler getAuthenticationFailureHandler() { + if (this.authenticationFailureHandler == null) { + this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error"); + } + return this.authenticationFailureHandler; + } + + /** + * Specifies {@link ReactiveAuthenticationManager} for one time tokens. Default + * implementation is {@link OneTimeTokenReactiveAuthenticationManager} + * @param authenticationManager + */ + public OneTimeTokenLoginSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.authenticationManager = authenticationManager; + return this; + } + + ReactiveAuthenticationManager getAuthenticationManager() { + if (this.authenticationManager == null) { + ReactiveUserDetailsService userDetailsService = getBean(ReactiveUserDetailsService.class); + return new OneTimeTokenReactiveAuthenticationManager(getTokenService(), userDetailsService); + } + return this.authenticationManager; + } + + /** + * Configures the {@link ReactiveOneTimeTokenService} used to generate and consume + * {@link OneTimeToken} + * @param oneTimeTokenService + */ + public OneTimeTokenLoginSpec tokenService(ReactiveOneTimeTokenService oneTimeTokenService) { + Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null"); + this.tokenService = oneTimeTokenService; + return this; + } + + ReactiveOneTimeTokenService getTokenService() { + if (this.tokenService != null) { + return this.tokenService; + } + ReactiveOneTimeTokenService oneTimeTokenService = getBeanOrNull(ReactiveOneTimeTokenService.class); + if (oneTimeTokenService != null) { + return oneTimeTokenService; + } + this.tokenService = new InMemoryReactiveOneTimeTokenService(); + return this.tokenService; + } + + /** + * Use this {@link ServerAuthenticationConverter} when converting incoming + * requests to an {@link Authentication}. By default, the + * {@link ServerOneTimeTokenAuthenticationConverter} is used. + * @param authenticationConverter the {@link ServerAuthenticationConverter} to use + */ + public OneTimeTokenLoginSpec authenticationConverter(ServerAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + return this; + } + + /** + * Specifies the URL to process the login request, defaults to {@code /login/ott}. + * Only POST requests are processed, for that reason make sure that you pass a + * valid CSRF token if CSRF protection is enabled. + * @param loginProcessingUrl + */ + public OneTimeTokenLoginSpec loginProcessingUrl(String loginProcessingUrl) { + Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty"); + this.loginProcessingUrl = loginProcessingUrl; + return this; + } + + /** + * Configures whether the default one-time token submit page should be shown. This + * will prevent the {@link OneTimeTokenSubmitPageGeneratingWebFilter} to be + * configured. + * @param show + */ + public OneTimeTokenLoginSpec showDefaultSubmitPage(boolean show) { + this.submitPageEnabled = show; + return this; + } + + /** + * Sets the URL that the default submit page will be generated. Defaults to + * {@code /login/ott}. If you don't want to generate the default submit page you + * should use {@link #showDefaultSubmitPage(boolean)}. Note that this method + * always invoke {@link #showDefaultSubmitPage(boolean)} passing {@code true}. + * @param submitPageUrl + */ + public OneTimeTokenLoginSpec defaultSubmitPageUrl(String submitPageUrl) { + Assert.hasText(submitPageUrl, "submitPageUrl cannot be null or empty"); + this.defaultSubmitPageUrl = submitPageUrl; + showDefaultSubmitPage(true); + return this; + } + + /** + * Specifies strategy to be used to handle generated one-time tokens. + * @param oneTimeTokenGenerationSuccessHandler + */ + public OneTimeTokenLoginSpec tokenGenerationSuccessHandler( + ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) { + Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null"); + this.tokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler; + return this; + } + + /** + * Specifies the URL that a One-Time Token generate request will be processed. + * Defaults to {@code /ott/generate}. + * @param tokenGeneratingUrl + */ + public OneTimeTokenLoginSpec tokenGeneratingUrl(String tokenGeneratingUrl) { + Assert.hasText(tokenGeneratingUrl, "tokenGeneratingUrl cannot be null or empty"); + this.tokenGeneratingUrl = tokenGeneratingUrl; + return this; + } + + /** + * The {@link ServerSecurityContextRepository} used to save the + * {@code Authentication}. Defaults to + * {@link WebSessionServerSecurityContextRepository}. For the + * {@code SecurityContext} to be loaded on subsequent requests the + * {@link ReactorContextWebFilter} must be configured to be able to load the value + * (they are not implicitly linked). + * @param securityContextRepository the repository to use + * @return the {@link OneTimeTokenLoginSpec} to continue configuring + */ + public OneTimeTokenLoginSpec securityContextRepository( + ServerSecurityContextRepository securityContextRepository) { + this.securityContextRepository = securityContextRepository; + return this; + } + + private ServerOneTimeTokenGenerationSuccessHandler getTokenGenerationSuccessHandler() { + if (this.tokenGenerationSuccessHandler == null) { + this.tokenGenerationSuccessHandler = getBeanOrNull(ServerOneTimeTokenGenerationSuccessHandler.class); + } + if (this.tokenGenerationSuccessHandler == null) { + throw new IllegalStateException(""" + A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin(). + Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL. + """); + } + return this.tokenGenerationSuccessHandler; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java index e833c6f3f77..edb6646b363 100644 --- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -305,6 +305,8 @@ static class MessageSecurityPostProcessor implements BeanDefinitionRegistryPostP private static final String CUSTOM_ARG_RESOLVERS_PROP = "customArgumentResolvers"; + private static final String TEMPLATE_EXPRESSION_BEAN_ID = "annotationExpressionTemplateDefaults"; + private final String inboundSecurityInterceptorId; private final boolean sameOriginDisabled; @@ -327,7 +329,13 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t if (current != null) { argResolvers.addAll((ManagedList) current.getValue()); } - argResolvers.add(new RootBeanDefinition(AuthenticationPrincipalArgumentResolver.class)); + RootBeanDefinition beanDefinition = new RootBeanDefinition( + AuthenticationPrincipalArgumentResolver.class); + if (registry.containsBeanDefinition(TEMPLATE_EXPRESSION_BEAN_ID)) { + beanDefinition.getPropertyValues() + .add("templateDefaults", new RuntimeBeanReference(TEMPLATE_EXPRESSION_BEAN_ID)); + } + argResolvers.add(beanDefinition); bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers); if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) { PropertyValue pathMatcherProp = bd.getPropertyValues().getPropertyValue("pathMatcher"); diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt index 8bb9fedf109..64249d7c80a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt @@ -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. @@ -16,17 +16,21 @@ package org.springframework.security.config.annotation.web +import org.springframework.context.ApplicationContext import org.springframework.http.HttpMethod +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy +import org.springframework.security.access.hierarchicalroles.RoleHierarchy import org.springframework.security.authorization.AuthenticatedAuthorizationManager import org.springframework.security.authorization.AuthorityAuthorizationManager import org.springframework.security.authorization.AuthorizationDecision import org.springframework.security.authorization.AuthorizationManager import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer +import org.springframework.security.config.core.GrantedAuthorityDefaults import org.springframework.security.core.Authentication +import org.springframework.security.web.access.IpAddressAuthorizationManager import org.springframework.security.web.access.intercept.AuthorizationFilter import org.springframework.security.web.access.intercept.RequestAuthorizationContext -import org.springframework.security.web.access.IpAddressAuthorizationManager import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher @@ -41,10 +45,29 @@ import java.util.function.Supplier * @since 5.7 * @property shouldFilterAllDispatcherTypes whether the [AuthorizationFilter] should filter all dispatcher types */ -class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { +class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { + @Deprecated(""" + Add authorization rules to DispatcherType directly. + + @Configuration + @EnableWebSecurity + public class SecurityConfig { + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() + // ... + ); + return http.build(); + } + } + """) var shouldFilterAllDispatcherTypes: Boolean? = null private val authorizationRules = mutableListOf() + private val rolePrefix: String + private val roleHierarchy: RoleHierarchy private val HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector" private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" @@ -190,7 +213,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { * @return the [AuthorizationManager] with the provided authority */ fun hasAuthority(authority: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAuthority(authority) + val manager = AuthorityAuthorizationManager.hasAuthority(authority) + return withRoleHierarchy(manager) } /** @@ -200,7 +224,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { * @return the [AuthorizationManager] with the provided authorities */ fun hasAnyAuthority(vararg authorities: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAnyAuthority(*authorities) + val manager = AuthorityAuthorizationManager.hasAnyAuthority(*authorities) + return withRoleHierarchy(manager) } /** @@ -210,7 +235,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { * @return the [AuthorizationManager] with the provided role */ fun hasRole(role: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasRole(role) + val manager = AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(role)) + return withRoleHierarchy(manager) } /** @@ -220,7 +246,8 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { * @return the [AuthorizationManager] with the provided roles */ fun hasAnyRole(vararg roles: String): AuthorizationManager { - return AuthorityAuthorizationManager.hasAnyRole(*roles) + val manager = AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, arrayOf(*roles)) + return withRoleHierarchy(manager) } /** @@ -273,4 +300,37 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() { } } } + + constructor() { + this.rolePrefix = "ROLE_" + this.roleHierarchy = NullRoleHierarchy() + } + + constructor(context: ApplicationContext) { + val rolePrefix = resolveRolePrefix(context) + this.rolePrefix = rolePrefix + val roleHierarchy = resolveRoleHierarchy(context) + this.roleHierarchy = roleHierarchy + } + + private fun resolveRolePrefix(context: ApplicationContext): String { + val beanNames = context.getBeanNamesForType(GrantedAuthorityDefaults::class.java) + if (beanNames.isNotEmpty()) { + return context.getBean(GrantedAuthorityDefaults::class.java).rolePrefix + } + return "ROLE_"; + } + + private fun resolveRoleHierarchy(context: ApplicationContext): RoleHierarchy { + val beanNames = context.getBeanNamesForType(RoleHierarchy::class.java) + if (beanNames.isNotEmpty()) { + return context.getBean(RoleHierarchy::class.java) + } + return NullRoleHierarchy() + } + + private fun withRoleHierarchy(manager: AuthorityAuthorizationManager): AuthorityAuthorizationManager { + manager.setRoleHierarchy(this.roleHierarchy) + return manager + } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt index f13ce656b15..528f8021e91 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.annotation.web import jakarta.servlet.Filter import jakarta.servlet.http.HttpServletRequest -import org.checkerframework.checker.units.qual.C import org.springframework.context.ApplicationContext import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.SecurityConfigurerAdapter @@ -60,7 +59,7 @@ import org.springframework.security.web.util.matcher.RequestMatcher * @param httpConfiguration the configurations to apply to [HttpSecurity] */ operator fun HttpSecurity.invoke(httpConfiguration: HttpSecurityDsl.() -> Unit) = - HttpSecurityDsl(this, httpConfiguration).build() + HttpSecurityDsl(this, httpConfiguration).build() /** * An [HttpSecurity] Kotlin DSL created by [`http { }`][invoke] @@ -77,6 +76,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" var authenticationManager: AuthenticationManager? = null + val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java) /** * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity] @@ -103,7 +103,10 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * @param configurer * the [SecurityConfigurerAdapter] for further customizations */ - fun > apply(configurer: C, configuration: C.() -> Unit = { }): C { + fun > apply( + configurer: C, + configuration: C.() -> Unit = { } + ): C { return this.http.apply(configurer).apply(configuration) } @@ -133,7 +136,10 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * the [HttpSecurity] for further customizations * @since 6.2 */ - fun > with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? { + fun > with( + configurer: C, + configuration: C.() -> Unit = { } + ): HttpSecurity? { return this.http.with(configurer, configuration) } @@ -263,6 +269,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * access for requests * @see [AuthorizeRequestsDsl] */ + @Deprecated(message = "Since 6.4. Use authorizeHttpRequests instead") fun authorizeRequests(authorizeRequestsConfiguration: AuthorizeRequestsDsl.() -> Unit) { val authorizeRequestsCustomizer = AuthorizeRequestsDsl().apply(authorizeRequestsConfiguration).get() this.http.authorizeRequests(authorizeRequestsCustomizer) @@ -297,7 +304,8 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * @since 5.7 */ fun authorizeHttpRequests(authorizeHttpRequestsConfiguration: AuthorizeHttpRequestsDsl.() -> Unit) { - val authorizeHttpRequestsCustomizer = AuthorizeHttpRequestsDsl().apply(authorizeHttpRequestsConfiguration).get() + val authorizeHttpRequestsCustomizer = + AuthorizeHttpRequestsDsl(this.context).apply(authorizeHttpRequestsConfiguration).get() this.http.authorizeHttpRequests(authorizeHttpRequestsCustomizer) } @@ -770,42 +778,42 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.saml2Logout(saml2LogoutCustomizer) } - /** - * Configures a SAML 2.0 relying party metadata endpoint. - * - * A [RelyingPartyRegistrationRepository] is required and must be registered with - * the [ApplicationContext] or configured via - * [Saml2Dsl.relyingPartyRegistrationRepository] - * - * Example: - * - * The following example shows the minimal configuration required, using a - * hypothetical asserting party. - * - * ``` - * @Configuration - * @EnableWebSecurity - * class SecurityConfig { - * - * @Bean - * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - * http { - * saml2Login { } - * saml2Metadata { } - * } - * return http.build() - * } - * } - * ``` - * @param saml2MetadataConfiguration custom configuration to configure the - * SAML2 relying party metadata endpoint - * @see [Saml2MetadataDsl] - * @since 6.1 - */ - fun saml2Metadata(saml2MetadataConfiguration: Saml2MetadataDsl.() -> Unit) { - val saml2MetadataCustomizer = Saml2MetadataDsl().apply(saml2MetadataConfiguration).get() - this.http.saml2Metadata(saml2MetadataCustomizer) - } + /** + * Configures a SAML 2.0 relying party metadata endpoint. + * + * A [RelyingPartyRegistrationRepository] is required and must be registered with + * the [ApplicationContext] or configured via + * [Saml2Dsl.relyingPartyRegistrationRepository] + * + * Example: + * + * The following example shows the minimal configuration required, using a + * hypothetical asserting party. + * + * ``` + * @Configuration + * @EnableWebSecurity + * class SecurityConfig { + * + * @Bean + * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + * http { + * saml2Login { } + * saml2Metadata { } + * } + * return http.build() + * } + * } + * ``` + * @param saml2MetadataConfiguration custom configuration to configure the + * SAML2 relying party metadata endpoint + * @see [Saml2MetadataDsl] + * @since 6.1 + */ + fun saml2Metadata(saml2MetadataConfiguration: Saml2MetadataDsl.() -> Unit) { + val saml2MetadataCustomizer = Saml2MetadataDsl().apply(saml2MetadataConfiguration).get() + this.http.saml2Metadata(saml2MetadataCustomizer) + } /** * Allows configuring how an anonymous user is represented. @@ -963,6 +971,36 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.oidcLogout(oidcLogoutCustomizer) } + /** + * Configures One-Time Token Login Support. + * + * Example: + * + * ``` + * @Configuration + * @EnableWebSecurity + * class SecurityConfig { + * + * @Bean + * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + * http { + * oneTimeTokenLogin { + * oneTimeTokenGenerationSuccessHandler = MyMagicLinkOneTimeTokenGenerationSuccessHandler() + * } + * } + * return http.build() + * } + * } + * + * ``` + * @since 6.4 + * @param oneTimeTokenLoginConfiguration custom configuration to configure one-time token login + */ + fun oneTimeTokenLogin(oneTimeTokenLoginConfiguration: OneTimeTokenLoginDsl.() -> Unit) { + val oneTimeTokenLoginCustomizer = OneTimeTokenLoginDsl().apply(oneTimeTokenLoginConfiguration).get() + this.http.oneTimeTokenLogin(oneTimeTokenLoginCustomizer) + } + /** * Configures Remember Me authentication. * @@ -993,6 +1031,37 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.rememberMe(rememberMeCustomizer) } + /** + * Enable WebAuthn configuration. + * + * Example: + * + * ``` + * @Configuration + * @EnableWebSecurity + * class SecurityConfig { + * + * @Bean + * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + * http { + * webAuthn { + * loginPage = "/log-in" + * } + * } + * return http.build() + * } + * } + * ``` + * + * @param webAuthnConfiguration custom configurations to be applied + * to the WebAuthn authentication + * @see [WebAuthnDsl] + */ + fun webAuthn(webAuthnConfiguration: WebAuthnDsl.() -> Unit) { + val webAuthnCustomizer = WebAuthnDsl().apply(webAuthnConfiguration).get() + this.http.webAuthn(webAuthnCustomizer) + } + /** * Adds the [Filter] at the location of the specified [Filter] class. * @@ -1048,7 +1117,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * (i.e. known) with Spring Security. */ @Suppress("DEPRECATION") - inline fun addFilterAt(filter: Filter) { + inline fun addFilterAt(filter: Filter) { this.addFilterAt(filter, T::class.java) } @@ -1107,7 +1176,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * (i.e. known) with Spring Security. */ @Suppress("DEPRECATION") - inline fun addFilterAfter(filter: Filter) { + inline fun addFilterAfter(filter: Filter) { this.addFilterAfter(filter, T::class.java) } @@ -1166,7 +1235,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu * (i.e. known) with Spring Security. */ @Suppress("DEPRECATION") - inline fun addFilterBefore(filter: Filter) { + inline fun addFilterBefore(filter: Filter) { this.addFilterBefore(filter, T::class.java) } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDsl.kt index 538c68ee116..8f151bfc8b4 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDsl.kt @@ -16,19 +16,20 @@ package org.springframework.security.config.annotation.web +import jakarta.servlet.http.HttpServletRequest import org.springframework.security.authentication.AuthenticationDetailsSource import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer import org.springframework.security.config.annotation.web.oauth2.login.AuthorizationEndpointDsl import org.springframework.security.config.annotation.web.oauth2.login.RedirectionEndpointDsl import org.springframework.security.config.annotation.web.oauth2.login.TokenEndpointDsl import org.springframework.security.config.annotation.web.oauth2.login.UserInfoEndpointDsl -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository import org.springframework.security.web.authentication.AuthenticationFailureHandler import org.springframework.security.web.authentication.AuthenticationSuccessHandler -import jakarta.servlet.http.HttpServletRequest /** * A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 login using idiomatic Kotlin code. @@ -61,6 +62,7 @@ class OAuth2LoginDsl { var loginProcessingUrl: String? = null var permitAll: Boolean? = null var authenticationDetailsSource: AuthenticationDetailsSource? = null + var oidcSessionRegistry: OidcSessionRegistry? = null private var defaultSuccessUrlOption: Pair? = null private var authorizationEndpoint: ((OAuth2LoginConfigurer.AuthorizationEndpointConfig) -> Unit)? = null @@ -236,6 +238,7 @@ class OAuth2LoginDsl { redirectionEndpoint?.also { oauth2Login.redirectionEndpoint(redirectionEndpoint) } userInfoEndpoint?.also { oauth2Login.userInfoEndpoint(userInfoEndpoint) } authenticationDetailsSource?.also { oauth2Login.authenticationDetailsSource(authenticationDetailsSource) } + oidcSessionRegistry?.also { oauth2Login.oidcSessionRegistry(oidcSessionRegistry) } } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt index f9fdd7dc4dd..27532b4c015 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt @@ -1,3 +1,4 @@ + /* * Copyright 2002-2023 the original author or authors. * @@ -72,4 +73,5 @@ class OidcLogoutDsl { backChannel?.also { oidcLogout.backChannel(backChannel) } } } + } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt new file mode 100644 index 00000000000..025e65e7410 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web + +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.ott.OneTimeTokenService +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.ott.OneTimeTokenLoginConfigurer +import org.springframework.security.web.authentication.AuthenticationConverter +import org.springframework.security.web.authentication.AuthenticationFailureHandler +import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler + +/** + * A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 login using idiomatic Kotlin code. + * + * @author Max Batischev + * @since 6.4 + * @property tokenService configures the [OneTimeTokenService] used to generate and consume + * @property authenticationConverter Use this [AuthenticationConverter] when converting incoming requests to an authentication + * @property authenticationFailureHandler the [AuthenticationFailureHandler] to use when authentication + * @property authenticationSuccessHandler the [AuthenticationSuccessHandler] to be used + * @property defaultSubmitPageUrl sets the URL that the default submit page will be generated + * @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown + * @property loginProcessingUrl the URL to process the login request + * @property tokenGeneratingUrl the URL that a One-Time Token generate request will be processed + * @property oneTimeTokenGenerationSuccessHandler the strategy to be used to handle generated one-time tokens + * @property authenticationProvider the [AuthenticationProvider] to use when authenticating the user + */ +@SecurityMarker +class OneTimeTokenLoginDsl { + var tokenService: OneTimeTokenService? = null + var authenticationConverter: AuthenticationConverter? = null + var authenticationFailureHandler: AuthenticationFailureHandler? = null + var authenticationSuccessHandler: AuthenticationSuccessHandler? = null + var defaultSubmitPageUrl: String? = null + var loginProcessingUrl: String? = null + var tokenGeneratingUrl: String? = null + var showDefaultSubmitPage: Boolean? = true + var oneTimeTokenGenerationSuccessHandler: OneTimeTokenGenerationSuccessHandler? = null + var authenticationProvider: AuthenticationProvider? = null + + internal fun get(): (OneTimeTokenLoginConfigurer) -> Unit { + return { oneTimeTokenLoginConfigurer -> + tokenService?.also { oneTimeTokenLoginConfigurer.tokenService(tokenService) } + authenticationConverter?.also { oneTimeTokenLoginConfigurer.authenticationConverter(authenticationConverter) } + authenticationFailureHandler?.also { + oneTimeTokenLoginConfigurer.authenticationFailureHandler( + authenticationFailureHandler + ) + } + authenticationSuccessHandler?.also { + oneTimeTokenLoginConfigurer.authenticationSuccessHandler( + authenticationSuccessHandler + ) + } + defaultSubmitPageUrl?.also { oneTimeTokenLoginConfigurer.defaultSubmitPageUrl(defaultSubmitPageUrl) } + showDefaultSubmitPage?.also { oneTimeTokenLoginConfigurer.showDefaultSubmitPage(showDefaultSubmitPage!!) } + loginProcessingUrl?.also { oneTimeTokenLoginConfigurer.loginProcessingUrl(loginProcessingUrl) } + tokenGeneratingUrl?.also { oneTimeTokenLoginConfigurer.tokenGeneratingUrl(tokenGeneratingUrl) } + oneTimeTokenGenerationSuccessHandler?.also { + oneTimeTokenLoginConfigurer.tokenGenerationSuccessHandler( + oneTimeTokenGenerationSuccessHandler + ) + } + authenticationProvider?.also { oneTimeTokenLoginConfigurer.authenticationProvider(authenticationProvider) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2Dsl.kt index 810bf54447a..e8f52dd44ac 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2Dsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2Dsl.kt @@ -48,6 +48,7 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand class Saml2Dsl { var relyingPartyRegistrationRepository: RelyingPartyRegistrationRepository? = null var loginPage: String? = null + var authenticationRequestUriQuery: String? = null var authenticationSuccessHandler: AuthenticationSuccessHandler? = null var authenticationFailureHandler: AuthenticationFailureHandler? = null var failureUrl: String? = null @@ -88,6 +89,9 @@ class Saml2Dsl { defaultSuccessUrlOption?.also { saml2Login.defaultSuccessUrl(defaultSuccessUrlOption!!.first, defaultSuccessUrlOption!!.second) } + authenticationRequestUriQuery?.also { + saml2Login.authenticationRequestUriQuery(authenticationRequestUriQuery) + } authenticationSuccessHandler?.also { saml2Login.successHandler(authenticationSuccessHandler) } authenticationFailureHandler?.also { saml2Login.failureHandler(authenticationFailureHandler) } authenticationManager?.also { saml2Login.authenticationManager(authenticationManager) } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/WebAuthnDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/WebAuthnDsl.kt new file mode 100644 index 00000000000..f1a9600f000 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/WebAuthnDsl.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer + +/** + * A Kotlin DSL to configure [HttpSecurity] webauthn using idiomatic Kotlin code. + * @property rpName the relying party name + * @property rpId the relying party id + * @property the allowed origins + * @property disableDefaultRegistrationPage disable default webauthn registration page + * @since 6.4 + * @author Rob Winch + * @author Max Batischev + */ +@SecurityMarker +class WebAuthnDsl { + var rpName: String? = null + var rpId: String? = null + var allowedOrigins: Set? = null + var disableDefaultRegistrationPage: Boolean? = false + + internal fun get(): (WebAuthnConfigurer) -> Unit { + return { webAuthn -> + rpName?.also { webAuthn.rpName(rpName) } + rpId?.also { webAuthn.rpId(rpId) } + allowedOrigins?.also { webAuthn.allowedOrigins(allowedOrigins) } + disableDefaultRegistrationPage?.also { webAuthn.disableDefaultRegistrationPage(disableDefaultRegistrationPage!!) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt index 85652b8fa7d..f36897604a4 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt @@ -53,6 +53,7 @@ class X509Dsl { var authenticationUserDetailsService: AuthenticationUserDetailsService? = null var subjectPrincipalRegex: String? = null + internal fun get(): (X509Configurer) -> Unit { return { x509 -> x509AuthenticationFilter?.also { x509.x509AuthenticationFilter(x509AuthenticationFilter) } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt index efac77a5667..f23b14d9811 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt @@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.oauth2.login import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer +import org.springframework.security.web.authentication.logout.LogoutHandler /** * A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using @@ -28,7 +29,26 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli */ @OAuth2LoginSecurityMarker class OidcBackChannelLogoutDsl { + private var _logoutUri: String? = null + private var _logoutHandler: LogoutHandler? = null + + var logoutHandler: LogoutHandler? + get() = _logoutHandler + set(value) { + _logoutHandler = value + _logoutUri = null + } + var logoutUri: String? + get() = _logoutUri + set(value) { + _logoutUri = value + _logoutHandler = null + } + internal fun get(): (OidcLogoutConfigurer.BackChannelLogoutConfigurer) -> Unit { - return { backChannel -> } + return { backChannel -> + logoutHandler?.also { backChannel.logoutHandler(logoutHandler) } + logoutUri?.also { backChannel.logoutUri(logoutUri) } + } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt index 639130f17fd..b904a79ad4a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt @@ -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. @@ -19,6 +19,7 @@ package org.springframework.security.config.web.server import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.context.ServerSecurityContextRepository import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.WebFilter @@ -65,6 +66,7 @@ operator fun ServerHttpSecurity.invoke(httpConfiguration: ServerHttpSecurityDsl. class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val init: ServerHttpSecurityDsl.() -> Unit) { var authenticationManager: ReactiveAuthenticationManager? = null + var securityContextRepository: ServerSecurityContextRepository? = null /** * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the @@ -712,12 +714,43 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in this.http.sessionManagement(sessionManagementCustomizer) } + /** + * Configures One-Time Token Login support. + * + * Example: + * + * ``` + * @Configuration + * @EnableWebFluxSecurity + * open class SecurityConfig { + * + * @Bean + * open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * oneTimeTokenLogin { + * tokenGenerationSuccessHandler = MyMagicLinkServerOneTimeTokenGenerationSuccessHandler() + * } + * } + * } + * } + * ``` + * + * @param oneTimeTokenLoginConfiguration custom configuration to configure the One-Time Token Login + * @since 6.4 + * @see [ServerOneTimeTokenLoginDsl] + */ + fun oneTimeTokenLogin(oneTimeTokenLoginConfiguration: ServerOneTimeTokenLoginDsl.()-> Unit){ + val oneTimeTokenLoginCustomizer = ServerOneTimeTokenLoginDsl().apply(oneTimeTokenLoginConfiguration).get() + this.http.oneTimeTokenLogin(oneTimeTokenLoginCustomizer) + } + /** * Apply all configurations to the provided [ServerHttpSecurity] */ internal fun build(): SecurityWebFilterChain { init() authenticationManager?.also { this.http.authenticationManager(authenticationManager) } + securityContextRepository?.also { this.http.securityContextRepository(securityContextRepository) } return this.http.build() } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt index 4ab8fcb0e45..6050c8bc039 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt @@ -19,6 +19,7 @@ package org.springframework.security.config.web.server import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.core.Authentication import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver @@ -53,6 +54,7 @@ import org.springframework.web.server.ServerWebExchange * @property authorizationRedirectStrategy the redirect strategy for Authorization Endpoint redirect URI. * @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an * authentication request. + * @property loginPage the URL to send users to if login is required. */ @ServerSecurityMarker class ServerOAuth2LoginDsl { @@ -68,6 +70,8 @@ class ServerOAuth2LoginDsl { var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null var authorizationRedirectStrategy: ServerRedirectStrategy? = null var authenticationMatcher: ServerWebExchangeMatcher? = null + var loginPage: String? = null + var oidcSessionRegistry: ReactiveOidcSessionRegistry? = null internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit { return { oauth2Login -> @@ -83,6 +87,8 @@ class ServerOAuth2LoginDsl { authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) } authorizationRedirectStrategy?.also { oauth2Login.authorizationRedirectStrategy(authorizationRedirectStrategy) } authenticationMatcher?.also { oauth2Login.authenticationMatcher(authenticationMatcher) } + loginPage?.also { oauth2Login.loginPage(loginPage) } + oidcSessionRegistry?.also { oauth2Login.oidcSessionRegistry(oidcSessionRegistry) } } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt index 5a245e5092e..ba6b15da933 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt @@ -16,6 +16,8 @@ package org.springframework.security.config.web.server +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler + /** * A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code. * @@ -24,7 +26,26 @@ package org.springframework.security.config.web.server */ @ServerSecurityMarker class ServerOidcBackChannelLogoutDsl { + private var _logoutUri: String? = null + private var _logoutHandler: ServerLogoutHandler? = null + + var logoutHandler: ServerLogoutHandler? + get() = _logoutHandler + set(value) { + _logoutHandler = value + _logoutUri = null + } + var logoutUri: String? + get() = _logoutUri + set(value) { + _logoutUri = value + _logoutHandler = null + } + internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit { - return { backChannel -> } + return { backChannel -> + logoutHandler?.also { backChannel.logoutHandler(logoutHandler) } + logoutUri?.also { backChannel.logoutUri(logoutUri) } + } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt index 503a5b0c843..7c27e3e0812 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt @@ -47,7 +47,9 @@ class ServerOidcLogoutDsl { * return http { * oauth2Login { } * oidcLogout { - * backChannel { } + * backChannel { + * sessionLogout { } + * } * } * } * } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt new file mode 100644 index 00000000000..3765a3e11aa --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt @@ -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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler +import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler +import org.springframework.security.web.server.context.ServerSecurityContextRepository + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] form login using idiomatic Kotlin code. + * + * @author Max Batischev + * @since 6.4 + * @property tokenService configures the [ReactiveOneTimeTokenService] used to generate and consume + * @property authenticationManager configures the [ReactiveAuthenticationManager] used to generate and consume + * @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication + * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication + * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used + * @property defaultSubmitPageUrl sets the URL that the default submit page will be generated + * @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown + * @property loginProcessingUrl the URL to process the login request + * @property tokenGeneratingUrl the URL that a One-Time Token generate request will be processed + * @property tokenGenerationSuccessHandler the strategy to be used to handle generated one-time tokens + * @property securityContextRepository the [ServerSecurityContextRepository] used to save the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the [ReactorContextWebFilter] must be configured to be able to load the value (they are not implicitly linked). + */ +@ServerSecurityMarker +class ServerOneTimeTokenLoginDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var tokenService: ReactiveOneTimeTokenService? = null + var authenticationConverter: ServerAuthenticationConverter? = null + var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null + var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null + var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null + var securityContextRepository: ServerSecurityContextRepository? = null + var defaultSubmitPageUrl: String? = null + var loginProcessingUrl: String? = null + var tokenGeneratingUrl: String? = null + var showDefaultSubmitPage: Boolean? = true + + internal fun get(): (ServerHttpSecurity.OneTimeTokenLoginSpec) -> Unit { + return { oneTimeTokenLogin -> + authenticationManager?.also { oneTimeTokenLogin.authenticationManager(authenticationManager) } + tokenService?.also { oneTimeTokenLogin.tokenService(tokenService) } + authenticationConverter?.also { oneTimeTokenLogin.authenticationConverter(authenticationConverter) } + authenticationFailureHandler?.also { + oneTimeTokenLogin.authenticationFailureHandler( + authenticationFailureHandler + ) + } + authenticationSuccessHandler?.also { + oneTimeTokenLogin.authenticationSuccessHandler( + authenticationSuccessHandler + ) + } + securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) } + defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) } + showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) } + loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) } + tokenGeneratingUrl?.also { oneTimeTokenLogin.tokenGeneratingUrl(tokenGeneratingUrl) } + tokenGenerationSuccessHandler?.also { + oneTimeTokenLogin.tokenGenerationSuccessHandler( + tokenGenerationSuccessHandler + ) + } + } + } +} diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index 90821ecf714..d91caab8644 100644 --- a/config/src/main/resources/META-INF/spring.schemas +++ b/config/src/main/resources/META-INF/spring.schemas @@ -1,4 +1,21 @@ -http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd +# +# 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. +# + +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.4.xsd +http\://www.springframework.org/schema/security/spring-security-6.4.xsd=org/springframework/security/config/spring-security-6.4.xsd http\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd http\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd http\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd @@ -23,7 +40,8 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd -https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd +https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.4.xsd +https\://www.springframework.org/schema/security/spring-security-6.4.xsd=org/springframework/security/config/spring-security-6.4.xsd https\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd https\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd https\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.4.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.4.rnc new file mode 100644 index 00000000000..9b2469aa879 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.4.rnc @@ -0,0 +1,1349 @@ +namespace a = "https://relaxng.org/ns/compatibility/annotations/1.0" +datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" + +default namespace = "http://www.springframework.org/schema/security" + +start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider + +hash = + ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + attribute hash {"bcrypt"} +base64 = + ## Whether a string should be base64 encoded + attribute base64 {xsd:boolean} +request-matcher = + ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. + attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} +port = + ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. + attribute port { xsd:nonNegativeInteger } +url = + ## Specifies a URL. + attribute url { xsd:token } +id = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute id {xsd:token} +name = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute name {xsd:token} +ref = + ## Defines a reference to a Spring bean Id. + attribute ref {xsd:token} + +cache-ref = + ## Defines a reference to a cache for use with a UserDetailsService. + attribute cache-ref {xsd:token} + +user-service-ref = + ## A reference to a user-service (or UserDetailsService bean) Id + attribute user-service-ref {xsd:token} + +authentication-manager-ref = + ## A reference to an AuthenticationManager bean + attribute authentication-manager-ref {xsd:token} + +data-source-ref = + ## A reference to a DataSource bean + attribute data-source-ref {xsd:token} + + + +debug = + ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. + element debug {empty} + +password-encoder = + ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. + element password-encoder {password-encoder.attlist} +password-encoder.attlist &= + ref | (hash) + +role-prefix = + ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. + attribute role-prefix {xsd:token} + +use-expressions = + ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. + attribute use-expressions {xsd:boolean} + +ldap-server = + ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. + element ldap-server {ldap-server.attlist} +ldap-server.attlist &= id? +ldap-server.attlist &= (url | port)? +ldap-server.attlist &= + ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. + attribute manager-dn {xsd:string}? +ldap-server.attlist &= + ## The password for the manager DN. This is required if the manager-dn is specified. + attribute manager-password {xsd:string}? +ldap-server.attlist &= + ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff + attribute ldif { xsd:string }? +ldap-server.attlist &= + ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + attribute root { xsd:string }? +ldap-server.attlist &= + ## Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and 'unboundid'. By default, it will depends if the library is available in the classpath. + attribute mode { "apacheds" | "unboundid" }? + +ldap-server-ref-attribute = + ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + attribute server-ref {xsd:token} + + +group-search-filter-attribute = + ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. + attribute group-search-filter {xsd:token} +group-search-base-attribute = + ## Search base for group membership searches. Defaults to "" (searching from the root). + attribute group-search-base {xsd:token} +user-search-filter-attribute = + ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. + attribute user-search-filter {xsd:token} +user-search-base-attribute = + ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + attribute user-search-base {xsd:token} +group-role-attribute-attribute = + ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". + attribute group-role-attribute {xsd:token} +user-details-class-attribute = + ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + attribute user-details-class {"person" | "inetOrgPerson"} +user-context-mapper-attribute = + ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + attribute user-context-mapper-ref {xsd:token} + + +ldap-user-service = + ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + element ldap-user-service {ldap-us.attlist} +ldap-us.attlist &= id? +ldap-us.attlist &= + ldap-server-ref-attribute? +ldap-us.attlist &= + user-search-filter-attribute? +ldap-us.attlist &= + user-search-base-attribute? +ldap-us.attlist &= + group-search-filter-attribute? +ldap-us.attlist &= + group-search-base-attribute? +ldap-us.attlist &= + group-role-attribute-attribute? +ldap-us.attlist &= + cache-ref? +ldap-us.attlist &= + role-prefix? +ldap-us.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +ldap-authentication-provider = + ## Sets up an ldap authentication provider + element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} +ldap-ap.attlist &= + ldap-server-ref-attribute? +ldap-ap.attlist &= + user-search-base-attribute? +ldap-ap.attlist &= + user-search-filter-attribute? +ldap-ap.attlist &= + group-search-base-attribute? +ldap-ap.attlist &= + group-search-filter-attribute? +ldap-ap.attlist &= + group-role-attribute-attribute? +ldap-ap.attlist &= + ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. + attribute user-dn-pattern {xsd:token}? +ldap-ap.attlist &= + role-prefix? +ldap-ap.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +password-compare-element = + ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user + element password-compare {password-compare.attlist, password-encoder?} + +password-compare.attlist &= + ## The attribute in the directory which contains the user password. Defaults to "userPassword". + attribute password-attribute {xsd:token}? +password-compare.attlist &= + hash? + +intercept-methods = + ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + element intercept-methods {intercept-methods.attlist, protect+} +intercept-methods.attlist &= + ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + attribute access-decision-manager-ref {xsd:token}? +intercept-methods.attlist &= + ## Use the AuthorizationManager API instead of AccessDecisionManager (defaults to true) + attribute use-authorization-manager {xsd:boolean}? +intercept-methods.attlist &= + ## Use this AuthorizationManager instead of the default (supercedes use-authorization-manager) + attribute authorization-manager-ref {xsd:token}? + +protect = + ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + element protect {protect.attlist, empty} +protect.attlist &= + ## A method name + attribute method {xsd:token} +protect.attlist &= + ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + attribute access {xsd:token} + +method-security-metadata-source = + ## Creates a MethodSecurityMetadataSource instance + element method-security-metadata-source {msmds.attlist, protect+} +msmds.attlist &= id? + +msmds.attlist &= use-expressions? + +method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. Also, annotation-based interception can be overridden by expressions listed in elements. + element method-security {method-security.attlist, expression-handler?, protect-pointcut*} +method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". + attribute pre-post-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false". + attribute secured-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false". + attribute jsr250-enabled {xsd:boolean}? +method-security.attlist &= + ## If true, class-based proxying will be used instead of interface-based proxying. + attribute proxy-target-class {xsd:boolean}? +method-security.attlist &= + ## If set to aspectj, then use AspectJ to intercept method invocation + attribute mode {"aspectj"}? +method-security.attlist &= + ## Specifies the security context holder strategy to use, by default uses a ThreadLocal-based strategy + attribute security-context-holder-strategy-ref {xsd:string}? +method-security.attlist &= + ## Use this ObservationRegistry to collect metrics on various parts of the filter chain + attribute observation-registry-ref {xsd:token}? + +global-method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. + element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". + attribute pre-post-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". + attribute secured-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". + attribute jsr250-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Optional AccessDecisionManager bean ID to override the default used for method security. + attribute access-decision-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor + attribute run-as-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Allows the advice "order" to be set for the method security interceptor. + attribute order {xsd:token}? +global-method-security.attlist &= + ## If true, class based proxying will be used instead of interface based proxying. + attribute proxy-target-class {xsd:boolean}? +global-method-security.attlist &= + ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. + attribute mode {"aspectj"}? +global-method-security.attlist &= + ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). + attribute metadata-source-ref {xsd:token}? +global-method-security.attlist &= + authentication-manager-ref? + + +after-invocation-provider = + ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. + element after-invocation-provider {ref} + +pre-post-annotation-handling = + ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. + element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} + +invocation-attribute-factory = + ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + element invocation-attribute-factory {ref} + +pre-invocation-advice = + ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. + element pre-invocation-advice {ref} + +post-invocation-advice = + ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. + element post-invocation-advice {ref} + + +expression-handler = + ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. + element expression-handler {ref} + +protect-pointcut = + ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. + element protect-pointcut {protect-pointcut.attlist, empty} +protect-pointcut.attlist &= + ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). + attribute expression {xsd:string} +protect-pointcut.attlist &= + ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" + attribute access {xsd:token} + +websocket-message-broker = + ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. + element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } + +websocket-message-broker.attrlist &= + ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. + attribute id {xsd:token}? +websocket-message-broker.attrlist &= + ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + attribute same-origin-disabled {xsd:boolean}? +websocket-message-broker.attrlist &= + ## Use this AuthorizationManager instead of deriving one from elements + attribute authorization-manager-ref {xsd:string}? +websocket-message-broker.attrlist &= + ## Use AuthorizationManager API instead of SecurityMetadatasource (defaults to true) + attribute use-authorization-manager {xsd:boolean}? +websocket-message-broker.attrlist &= + ## Use this SecurityContextHolderStrategy (note only supported in conjunction with the AuthorizationManager API) + attribute security-context-holder-strategy-ref {xsd:string}? + +intercept-message = + ## Creates an authorization rule for a websocket message. + element intercept-message {intercept-message.attrlist} + +intercept-message.attrlist &= + ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. + attribute pattern {xsd:token}? +intercept-message.attrlist &= + ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. + attribute access {xsd:token}? +intercept-message.attrlist &= + ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? + +http-firewall = + ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. + element http-firewall {ref} + +http = + ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & saml2-login? & saml2-logout? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } +http.attlist &= + ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. + attribute pattern {xsd:token}? +http.attlist &= + ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. + attribute security {"none"}? +http.attlist &= + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref { xsd:token }? +http.attlist &= + ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. + attribute auto-config {xsd:boolean}? +http.attlist &= + use-expressions? +http.attlist &= + ## A reference to a SecurityContextHolderStrategy bean. This can be used to customize how the SecurityContextHolder is stored during a request + attribute security-context-holder-strategy-ref {xsd:token}? +http.attlist &= + ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. + attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? +http.attlist &= + ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. + attribute security-context-repository-ref {xsd:token}? +http.attlist &= + ## Optional attribute that specifies that the SecurityContext should require explicit saving rather than being synchronized from the SecurityContextHolder. Defaults to "true". + attribute security-context-explicit-save {xsd:boolean}? +http.attlist &= + request-matcher? +http.attlist &= + ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". + attribute servlet-api-provision {xsd:boolean}? +http.attlist &= + ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + attribute jaas-api-provision {xsd:boolean}? +http.attlist &= + ## Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true) + attribute use-authorization-manager {xsd:boolean}? +http.attlist &= + ## Use this AuthorizationManager instead of deriving one from elements + attribute authorization-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. + attribute access-decision-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". + attribute realm {xsd:token}? +http.attlist &= + ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + attribute entry-point-ref {xsd:token}? +http.attlist &= + ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "false" + attribute once-per-request {xsd:boolean}? +http.attlist &= + ## Corresponds to the shouldFilterAllDispatcherTypes property of AuthorizationFilter. Do not work when use-authorization-manager=false. Defaults to "true". + attribute filter-all-dispatcher-types {xsd:boolean}? +http.attlist &= + ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). + attribute disable-url-rewriting {xsd:boolean}? +http.attlist &= + ## Exposes the list of filters defined by this configuration under this bean name in the application context. + name? +http.attlist &= + authentication-manager-ref? +http.attlist &= + ## Use this ObservationRegistry to collect metrics on various parts of the filter chain + attribute observation-registry-ref {xsd:token}? + +access-denied-handler = + ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. + element access-denied-handler {access-denied-handler.attlist, empty} +access-denied-handler.attlist &= (ref | access-denied-handler-page) + +access-denied-handler-page = + ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. + attribute error-page {xsd:token} + +intercept-url = + ## Specifies the access attributes and/or filter list for a particular set of URLs. + element intercept-url {intercept-url.attlist, empty} +intercept-url.attlist &= + (pattern | request-matcher-ref) +intercept-url.attlist &= + ## The access configuration attributes that apply for the configured path. + attribute access {xsd:token}? +intercept-url.attlist &= + ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. + attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? + +intercept-url.attlist &= + ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. + attribute requires-channel {xsd:token}? +intercept-url.attlist &= + ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. + attribute servlet-path {xsd:token}? + +logout = + ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. + element logout {logout.attlist, empty} +logout.attlist &= + ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. + attribute logout-url {xsd:token}? +logout.attlist &= + ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). + attribute logout-success-url {xsd:token}? +logout.attlist &= + ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. + attribute invalidate-session {xsd:boolean}? +logout.attlist &= + ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. + attribute success-handler-ref {xsd:token}? +logout.attlist &= + ## A comma-separated list of the names of cookies which should be deleted when the user logs out + attribute delete-cookies {xsd:token}? + +request-cache = + ## Allow the RequestCache used for saving requests during the login process to be set + element request-cache {ref} + +form-login = + ## Sets up a form login configuration for authentication with a username and password + element form-login {form-login.attlist, empty} +form-login.attlist &= + ## The URL that the login form is posted to. If unspecified, it defaults to /login. + attribute login-processing-url {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the username. Defaults to 'username'. + attribute username-parameter {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the password. Defaults to 'password'. + attribute password-parameter {xsd:token}? +form-login.attlist &= + ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. + attribute default-target-url {xsd:token}? +form-login.attlist &= + ## Whether the user should always be redirected to the default-target-url after login. + attribute always-use-default-target {xsd:boolean}? +form-login.attlist &= + ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. + attribute login-page {xsd:token}? +form-login.attlist &= + ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. + attribute authentication-failure-url {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination + attribute authentication-success-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination + attribute authentication-failure-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationFailureHandler + attribute authentication-failure-forward-url {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationSuccessHandler + attribute authentication-success-forward-url {xsd:token}? + +oauth2-login = + ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + element oauth2-login {oauth2-login.attlist} +oauth2-login.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the authorization RedirectStrategy + attribute authorization-redirect-strategy-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the GrantedAuthoritiesMapper + attribute user-authorities-mapper-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2UserService + attribute user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OpenID Connect OAuth2UserService + attribute oidc-user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +oauth2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + attribute jwt-decoder-factory-ref {xsd:token}? + +oauth2-client = + ## Configures OAuth 2.0 Client support. + element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } +oauth2-client.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? + +authorization-code-grant = + ## Configures OAuth 2.0 Authorization Code Grant. + element authorization-code-grant {authorization-code-grant.attlist, empty} +authorization-code-grant.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the authorization RedirectStrategy + attribute authorization-redirect-strategy-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? + +client-registrations = + ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registrations {client-registration+, provider*} + +client-registration = + ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registration {client-registration.attlist} +client-registration.attlist &= + ## The ID that uniquely identifies the client registration. + attribute registration-id {xsd:token} +client-registration.attlist &= + ## The client identifier. + attribute client-id {xsd:token} +client-registration.attlist &= + ## The client secret. + attribute client-secret {xsd:token}? +client-registration.attlist &= + ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). + attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? +client-registration.attlist &= + ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials and password. + attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password"}? +client-registration.attlist &= + ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. + attribute redirect-uri {xsd:token}? +client-registration.attlist &= + ## A comma-separated list of scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. + attribute scope {xsd:token}? +client-registration.attlist &= + ## A descriptive name used for the client. The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. + attribute client-name {xsd:token}? +client-registration.attlist &= + ## A reference to the associated provider. May reference a 'provider' element or use one of the common providers (google, github, facebook, okta). + attribute provider-id {xsd:token} + +provider = + ## The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + element provider {provider.attlist} +provider.attlist &= + ## The ID that uniquely identifies the provider. + attribute provider-id {xsd:token} +provider.attlist &= + ## The Authorization Endpoint URI for the Authorization Server. + attribute authorization-uri {xsd:token}? +provider.attlist &= + ## The Token Endpoint URI for the Authorization Server. + attribute token-uri {xsd:token}? +provider.attlist &= + ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. + attribute user-info-uri {xsd:token}? +provider.attlist &= + ## The authentication method used when sending the access token to the UserInfo Endpoint. The supported values are header, form and query. + attribute user-info-authentication-method {"header" | "form" | "query"}? +provider.attlist &= + ## The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + attribute user-info-user-name-attribute {xsd:token}? +provider.attlist &= + ## The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. + attribute jwk-set-uri {xsd:token}? +provider.attlist &= + ## The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + attribute issuer-uri {xsd:token}? + +oauth2-resource-server = + ## Configures authentication support as an OAuth 2.0 Resource Server. + element oauth2-resource-server {oauth2-resource-server.attlist, (jwt? & opaque-token?)} +oauth2-resource-server.attlist &= + ## Reference to an AuthenticationManagerResolver + attribute authentication-manager-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a BearerTokenResolver + attribute bearer-token-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a AuthenticationEntryPoint + attribute entry-point-ref {xsd:token}? + +jwt = + ## Configures JWT authentication + element jwt {jwt.attlist} +jwt.attlist &= + ## The URI to use to collect the JWK Set for verifying JWTs + attribute jwk-set-uri {xsd:token}? +jwt.attlist &= + ## Reference to a JwtDecoder + attribute decoder-ref {xsd:token}? +jwt.attlist &= + ## Reference to a Converter + attribute jwt-authentication-converter-ref {xsd:token}? + +opaque-token = + ## Configuration Opaque Token authentication + element opaque-token {opaque-token.attlist} +opaque-token.attlist &= + ## The URI to use to introspect opaque token attributes + attribute introspection-uri {xsd:token}? +opaque-token.attlist &= + ## The Client ID to use to authenticate the introspection request + attribute client-id {xsd:token}? +opaque-token.attlist &= + ## The Client secret to use to authenticate the introspection request + attribute client-secret {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenIntrospector + attribute introspector-ref {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication. + attribute authentication-converter-ref {xsd:token}? + +saml2-login = + ## Configures authentication support for SAML 2.0 Login + element saml2-login {saml2-login.attlist} +saml2-login.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestRepository + attribute authentication-request-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestResolver + attribute authentication-request-resolver-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationConverter + attribute authentication-converter-ref {xsd:token}? +saml2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +saml2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationManager + attribute authentication-manager-ref {xsd:token}? + +saml2-logout = + ## Configures SAML 2.0 Single Logout support + element saml2-logout {saml2-logout.attlist} +saml2-logout.attlist &= + ## The URL by which the relying or asserting party can trigger logout + attribute logout-url {xsd:token}? +saml2-logout.attlist &= + ## The URL by which the asserting party can send a SAML 2.0 Logout Request + attribute logout-request-url {xsd:token}? +saml2-logout.attlist &= + ## The URL by which the asserting party can send a SAML 2.0 Logout Response + attribute logout-response-url {xsd:token}? +saml2-logout.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestValidator + attribute logout-request-validator-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestResolver + attribute logout-request-resolver-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutRequestRepository + attribute logout-request-repository-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutResponseValidator + attribute logout-response-validator-ref {xsd:token}? +saml2-logout.attlist &= + ## Reference to the Saml2LogoutResponseResolver + attribute logout-response-resolver-ref {xsd:token}? + +relying-party-registrations = + ## Container element for relying party(ies) registered with a SAML 2.0 identity provider + element relying-party-registrations {relying-party-registrations.attlist, relying-party-registration+, asserting-party*} +relying-party-registrations.attlist &= + ## The identifier by which to refer to the repository in other beans + attribute id {xsd:token}? + +relying-party-registration = + ## Represents a relying party registered with a SAML 2.0 identity provider + element relying-party-registration {relying-party-registration.attlist, signing-credential*, decryption-credential*} +relying-party-registration.attlist &= + ## The ID that uniquely identifies the relying party registration. + attribute registration-id {xsd:token} +relying-party-registration.attlist &= + ## The location of the Identity Provider's metadata. + attribute metadata-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party's EntityID + attribute entity-id {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Location + attribute assertion-consumer-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Binding + attribute assertion-consumer-service-binding {xsd:token}? +relying-party-registration.attlist &= + ## A reference to the associated asserting party. + attribute asserting-party-id {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? + +signing-credential = + ## The relying party's signing credential + element signing-credential {signing-credential.attlist} +signing-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +signing-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +decryption-credential = + ## The relying party's decryption credential + element decryption-credential {decryption-credential.attlist} +decryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +decryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +asserting-party = + ## The configuration metadata of the Asserting party + element asserting-party {asserting-party.attlist, verification-credential*, encryption-credential*} +asserting-party.attlist &= + ## A unique identifier of the asserting party. + attribute asserting-party-id {xsd:token} +asserting-party.attlist &= + ## The asserting party's EntityID. + attribute entity-id {xsd:token} +asserting-party.attlist &= + ## Indicates the asserting party's preference that relying parties should sign the AuthnRequest before sending + attribute want-authn-requests-signed {xsd:token}? +asserting-party.attlist &= + ## The SingleSignOnService Location. + attribute single-sign-on-service-location {xsd:token} +asserting-party.attlist &= + ## The SingleSignOnService Binding. + attribute single-sign-on-service-binding {xsd:token}? +asserting-party.attlist &= + ## A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this asserting party, in preference order. + attribute signing-algorithms {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? + +verification-credential = + ## The relying party's verification credential + element verification-credential {verification-credential.attlist} +verification-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +verification-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +encryption-credential = + ## The asserting party's encryption credential + element encryption-credential {encryption-credential.attlist} +encryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +encryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + + +filter-chain-map = + ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + element filter-chain-map {filter-chain-map.attlist, filter-chain+} +filter-chain-map.attlist &= + request-matcher? + +filter-chain = + ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. + element filter-chain {filter-chain.attlist, empty} +filter-chain.attlist &= + (pattern | request-matcher-ref) +filter-chain.attlist &= + ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + attribute filters {xsd:token} + +pattern = + ## The request URL pattern which will be mapped to the FilterChain. + attribute pattern {xsd:token} +request-matcher-ref = + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref {xsd:token} + +filter-security-metadata-source = + ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. + element filter-security-metadata-source {fsmds.attlist, intercept-url+} +fsmds.attlist &= + use-expressions? +fsmds.attlist &= + id? +fsmds.attlist &= + request-matcher? + +http-basic = + ## Adds support for basic authentication + element http-basic {http-basic.attlist, empty} + +http-basic.attlist &= + ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + attribute entry-point-ref {xsd:token}? +http-basic.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +password-management = + ## Adds support for the password management. + element password-management {password-management.attlist, empty} + +password-management.attlist &= + ## The change password page. Defaults to "/change-password". + attribute change-password-page {xsd:string}? + +session-management = + ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. + element session-management {session-management.attlist, concurrency-control?} + +session-management.attlist &= + ## Specifies that SessionAuthenticationStrategy must be explicitly invoked. Default false (i.e. SessionManagementFilter will implicitly invoke SessionAuthenticationStrategy). + attribute authentication-strategy-explicit-invocation {xsd:boolean}? +session-management.attlist &= + ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. + attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? +session-management.attlist &= + ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. + attribute invalid-session-url {xsd:token}? +session-management.attlist &= + ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter + attribute invalid-session-strategy-ref {xsd:token}? +session-management.attlist &= + ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter + attribute session-authentication-strategy-ref {xsd:token}? +session-management.attlist &= + ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. + attribute session-authentication-error-url {xsd:token}? + + +concurrency-control = + ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. + element concurrency-control {concurrency-control.attlist, empty} + +concurrency-control.attlist &= + ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. + attribute max-sessions {xsd:token}? +concurrency-control.attlist &= + ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. + attribute expired-url {xsd:token}? +concurrency-control.attlist &= + ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter + attribute expired-session-strategy-ref {xsd:token}? +concurrency-control.attlist &= + ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. + attribute error-if-maximum-exceeded {xsd:boolean}? +concurrency-control.attlist &= + ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. + attribute session-registry-alias {xsd:token}? +concurrency-control.attlist &= + ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. + attribute session-registry-ref {xsd:token}? + + +remember-me = + ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. + element remember-me {remember-me.attlist} +remember-me.attlist &= + ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? + +remember-me.attlist &= + (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) + +remember-me.attlist &= + user-service-ref? + +remember-me.attlist &= + ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. + attribute services-alias {xsd:token}? + +remember-me.attlist &= + ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. + attribute use-secure-cookie {xsd:boolean}? + +remember-me.attlist &= + ## The period (in seconds) for which the remember-me cookie should be valid. + attribute token-validity-seconds {xsd:string}? + +remember-me.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. + attribute authentication-success-handler-ref {xsd:token}? +remember-me.attlist &= + ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-parameter {xsd:token}? +remember-me.attlist &= + ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-cookie {xsd:token}? + +token-repository-ref = + ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. + attribute token-repository-ref {xsd:token} +remember-me-services-ref = + ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. + attribute services-ref {xsd:token}? +remember-me-data-source-ref = + ## DataSource bean for the database that contains the token repository schema. + data-source-ref + +anonymous = + ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. + element anonymous {anonymous.attlist} +anonymous.attlist &= + ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? +anonymous.attlist &= + ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". + attribute username {xsd:token}? +anonymous.attlist &= + ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + attribute granted-authority {xsd:token}? +anonymous.attlist &= + ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. + attribute enabled {xsd:boolean}? + + +port-mappings = + ## Defines the list of mappings between http and https ports for use in redirects + element port-mappings {port-mappings.attlist, port-mapping+} + +port-mappings.attlist &= empty + +port-mapping = + ## Provides a method to map http ports to https ports when forcing a redirect. + element port-mapping {http-port, https-port} + +http-port = + ## The http port to use. + attribute http {xsd:token} + +https-port = + ## The https port to use. + attribute https {xsd:token} + + +x509 = + ## Adds support for X.509 client authentication. + element x509 {x509.attlist} +x509.attlist &= + ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". + attribute subject-principal-regex {xsd:token}? +x509.attlist &= + ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. + user-service-ref? +x509.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +jee = + ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. + element jee {jee.attlist} +jee.attlist &= + ## A comma-separate list of roles to look for in the incoming HttpServletRequest. + attribute mappable-roles {xsd:token} +jee.attlist &= + ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. + user-service-ref? + +authentication-manager = + ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. + element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} +authman.attlist &= + id? +authman.attlist &= + ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) + attribute alias {xsd:token}? +authman.attlist &= + ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. + attribute erase-credentials {xsd:boolean}? +authman.attlist &= + ## Use this ObservationRegistry to collect metrics on various parts of the filter chain + attribute observation-registry-ref {xsd:token}? + +authentication-provider = + ## Indicates that the contained user-service should be used as an authentication source. + element authentication-provider {ap.attlist & any-user-service & password-encoder?} +ap.attlist &= + ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. + ref? +ap.attlist &= + ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. + user-service-ref? + +user-service = + ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + element user-service {id? & (properties-file | (user*))} +properties-file = + ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + attribute properties {xsd:token}? + +user = + ## Represents a user in the application. + element user {user.attlist, empty} +user.attlist &= + ## The username assigned to the user. + attribute name {xsd:token} +user.attlist &= + ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. + attribute password {xsd:string}? +user.attlist &= + ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + attribute authorities {xsd:token} +user.attlist &= + ## Can be set to "true" to mark an account as locked and unusable. + attribute locked {xsd:boolean}? +user.attlist &= + ## Can be set to "true" to mark an account as disabled and unusable. + attribute disabled {xsd:boolean}? + +jdbc-user-service = + ## Causes creation of a JDBC-based UserDetailsService. + element jdbc-user-service {id? & jdbc-user-service.attlist} +jdbc-user-service.attlist &= + ## The bean ID of the DataSource which provides the required tables. + attribute data-source-ref {xsd:token} +jdbc-user-service.attlist &= + cache-ref? +jdbc-user-service.attlist &= + ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" + attribute users-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" + attribute authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + attribute group-authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + role-prefix? + +csrf = +## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. + element csrf {csrf-options.attlist} +csrf-options.attlist &= + ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). + attribute disabled {xsd:boolean}? +csrf-options.attlist &= + ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + attribute request-matcher-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository. + attribute token-repository-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRequestHandler to use. The default is CsrfTokenRequestAttributeHandler. + attribute request-handler-ref { xsd:token }? + +headers = +## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & cross-origin-opener-policy? & cross-origin-embedder-policy? & cross-origin-resource-policy? & header*)} +headers-options.attlist &= + ## Specifies if the default headers should be disabled. Default false. + attribute defaults-disabled {xsd:token}? +headers-options.attlist &= + ## Specifies if headers should be disabled. Default false. + attribute disabled {xsd:token}? +hsts = + ## Adds support for HTTP Strict Transport Security (HSTS) + element hsts {hsts-options.attlist} +hsts-options.attlist &= + ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hsts-options.attlist &= + ## Specifies if subdomains should be included. Default true. + attribute include-subdomains {xsd:boolean}? +hsts-options.attlist &= + ## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year. + attribute max-age-seconds {xsd:integer}? +hsts-options.attlist &= + ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. + attribute request-matcher-ref { xsd:token }? +hsts-options.attlist &= + ## Specifies if preload should be included. Default false. + attribute preload {xsd:boolean}? + +cors = +## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource +element cors { cors-options.attlist } +cors-options.attlist &= + ref? +cors-options.attlist &= + ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use + attribute configuration-source-ref {xsd:token}? + +hpkp = + ## Adds support for HTTP Public Key Pinning (HPKP). + element hpkp {hpkp.pins,hpkp.attlist} +hpkp.pins = + ## The list with pins + element pins {hpkp.pin+} +hpkp.pin = + ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute + element pin { + ## The cryptographic hash algorithm + attribute algorithm { xsd:string }?, + text + } +hpkp.attlist &= + ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hpkp.attlist &= + ## Specifies if subdomains should be included. Default false. + attribute include-subdomains {xsd:boolean}? +hpkp.attlist &= + ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + attribute max-age-seconds {xsd:integer}? +hpkp.attlist &= + ## Specifies if the browser should only report pin validation failures. Default true. + attribute report-only {xsd:boolean}? +hpkp.attlist &= + ## Specifies the URI to which the browser should report pin validation failures. + attribute report-uri {xsd:string}? + +content-security-policy = + ## Adds support for Content Security Policy (CSP) + element content-security-policy {csp-options.attlist} +csp-options.attlist &= + ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + attribute policy-directives {xsd:token}? +csp-options.attlist &= + ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + attribute report-only {xsd:boolean}? + +referrer-policy = + ## Adds support for Referrer Policy + element referrer-policy {referrer-options.attlist} +referrer-options.attlist &= + ## The policies for the Referrer-Policy header. + attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? + +feature-policy = + ## Adds support for Feature Policy + element feature-policy {feature-options.attlist} +feature-options.attlist &= + ## The security policy directive(s) for the Feature-Policy header. + attribute policy-directives {xsd:token}? + +permissions-policy = + ## Adds support for Permissions Policy + element permissions-policy {permissions-options.attlist} +permissions-options.attlist &= + ## The policies for the Permissions-Policy header. + attribute policy {xsd:token}? + +cache-control = + ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request + element cache-control {cache-control.attlist} +cache-control.attlist &= + ## Specifies if Cache Control should be disabled. Default false. + attribute disabled {xsd:boolean}? + +frame-options = + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} +frame-options.attlist &= + ## If disabled, the X-Frame-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? +frame-options.attlist &= + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? +frame-options.attlist &= + ## Specify the strategy to use when ALLOW-FROM is chosen. + attribute strategy {"static","whitelist","regexp"}? +frame-options.attlist &= + ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. + ref? +frame-options.attlist &= + ## Specify a value to use for the chosen strategy. + attribute value {xsd:string}? +frame-options.attlist &= + ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. + ## Deprecated ALLOW-FROM is an obsolete directive that no longer works in modern browsers. Instead use + ## Content-Security-Policy with the + ## frame-ancestors + ## directive. + attribute from-parameter {xsd:string}? + + +xss-protection = + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} +xss-protection.attlist &= + ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + attribute disabled {xsd:boolean}? +xss-protection.attlist &= + ## Specify the value for the X-Xss-Protection header. Defaults to "0". + attribute header-value {"0"|"1"|"1; mode=block"}? + +content-type-options = + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {content-type-options.attlist, empty} +content-type-options.attlist &= + ## If disabled, the X-Content-Type-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? + +cross-origin-opener-policy = + ## Adds support for Cross-Origin-Opener-Policy header + element cross-origin-opener-policy {cross-origin-opener-policy-options.attlist,empty} +cross-origin-opener-policy-options.attlist &= + ## The policies for the Cross-Origin-Opener-Policy header. + attribute policy {"unsafe-none","same-origin","same-origin-allow-popups"}? + +cross-origin-embedder-policy = + ## Adds support for Cross-Origin-Embedder-Policy header + element cross-origin-embedder-policy {cross-origin-embedder-policy-options.attlist,empty} +cross-origin-embedder-policy-options.attlist &= + ## The policies for the Cross-Origin-Embedder-Policy header. + attribute policy {"unsafe-none","require-corp"}? + +cross-origin-resource-policy = + ## Adds support for Cross-Origin-Resource-Policy header + element cross-origin-resource-policy {cross-origin-resource-policy-options.attlist,empty} +cross-origin-resource-policy-options.attlist &= + ## The policies for the Cross-Origin-Resource-Policy header. + attribute policy {"cross-origin","same-origin","same-site"}? + +header= + ## Add additional headers to the response. + element header {header.attlist} +header.attlist &= + ## The name of the header to add. + attribute name {xsd:token}? +header.attlist &= + ## The value for the header. + attribute value {xsd:token}? +header.attlist &= + ## Reference to a custom HeaderWriter implementation. + ref? + +any-user-service = user-service | jdbc-user-service | ldap-user-service + +custom-filter = + ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. + element custom-filter {custom-filter.attlist} + +custom-filter.attlist &= + ref + +custom-filter.attlist &= + (after | before | position) + +after = + ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. + attribute after {named-security-filter} +before = + ## The filter immediately before which the custom-filter should be placed in the chain + attribute before {named-security-filter} +position = + ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. + attribute position {named-security-filter} + +named-security-filter = "FIRST" | "DISABLE_ENCODE_URL_FILTER" | "FORCE_EAGER_SESSION_FILTER" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "SAML2_LOGOUT_REQUEST_FILTER" | "SAML2_LOGOUT_RESPONSE_FILTER" | "CSRF_FILTER" | "SAML2_LOGOUT_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "DEFAULT_RESOURCES_FILTER" | "LOGIN_PAGE_FILTER" | "LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.4.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.4.xsd new file mode 100644 index 00000000000..e46438d80dd --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.4.xsd @@ -0,0 +1,3822 @@ + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + Whether a string should be base64 encoded + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + + + Specifies a URL. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + A reference to an AuthenticationManager bean + + + + + + + + A reference to a DataSource bean + + + + + + + Enables Spring Security debugging infrastructure. This will provide human-readable + (multi-line) debugging information to monitor requests coming into the security filters. + This may include sensitive information, such as request parameters or headers, and should + only be used in a development environment. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Defines an LDAP server location or starts an embedded server. The url indicates the + location of a remote server. If no url is given, an embedded server will be started, + listening on the supplied port number. The port is optional and defaults to 33389. A + Spring LDAP ContextSource bean will be registered for the server with the id supplied. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Specifies a URL. + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + Username (DN) of the "manager" user identity which will be used to authenticate to a + (non-embedded) LDAP server. If omitted, anonymous access will be used. + + + + + + The password for the manager DN. This is required if the manager-dn is specified. + + + + + + Explicitly specifies an ldif file resource to load into an embedded LDAP server. The + default is classpath*:*.ldiff + + + + + + Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + + + + + + Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and + 'unboundid'. By default, it will depends if the library is available in the classpath. + + + + + + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + This element configures a LdapUserDetailsService which is a combination of a + FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key + "{0}" must be present and will be substituted with the username. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The attribute in the directory which contains the user password. Defaults to + "userPassword". + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + Can be used inside a bean definition to add a security interceptor to the bean and set up + access configuration attributes for the bean's methods + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + Optional AccessDecisionManager bean ID to be used by the created method security + interceptor. + + + + + + Use the AuthorizationManager API instead of AccessDecisionManager (defaults to true) + + + + + + Use this AuthorizationManager instead of the default (supercedes + use-authorization-manager) + + + + + + + + + A method name + + + + + + Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + + + + + + + Creates a MethodSecurityMetadataSource instance + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with Spring Security annotations. Where + there is a match, the beans will automatically be proxied and security authorization + applied to the methods accordingly. Interceptors are invoked in the order specified in + AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + Also, annotation-based interception can be overridden by expressions listed in + <protect-pointcut> elements. + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "true". + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "false". + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "false". + + + + + + If true, class-based proxying will be used instead of interface-based proxying. + + + + + + If set to aspectj, then use AspectJ to intercept method invocation + + + + + + + + + + + Specifies the security context holder strategy to use, by default uses a ThreadLocal-based + strategy + + + + + + Use this ObservationRegistry to collect metrics on various parts of the filter chain + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with the ordered list of + "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a + match, the beans will automatically be proxied and security authorization applied to the + methods accordingly. If you use and enable all four sources of method security metadata + (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 + security annotations), the metadata sources will be queried in that order. In practical + terms, this enables you to use XML to override method security metadata expressed in + annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize + etc.), @Secured and finally JSR-250. + + + + + + + + Allows the default expression-based mechanism for handling Spring Security's pre and post + invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be + replace entirely. Only applies if these annotations are enabled. + + + + + + + Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and + post invocation metadata from the annotated methods. + + + + + + + + + Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the + PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. + + + + + + + + + Customizes the PostInvocationAdviceProvider with the ref as the + PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + + + + Allows addition of extra AfterInvocationProvider beans which should be called by the + MethodSecurityInterceptor created by global-method-security. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "disabled". + + + + + + + + + + + + Optional AccessDecisionManager bean ID to override the default used for method security. + + + + + + Optional RunAsmanager implementation which will be used by the configured + MethodSecurityInterceptor + + + + + + Allows the advice "order" to be set for the method security interceptor. + + + + + + If true, class based proxying will be used instead of interface based proxying. + + + + + + Can be used to specify that AspectJ should be used instead of the default Spring AOP. If + set, secured classes must be woven with the AnnotationSecurityAspect from the + spring-security-aspects module. + + + + + + + + + + + An external MethodSecurityMetadataSource instance can be supplied which will take priority + over other sources (such as the default annotations). + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + + + + + + + An AspectJ expression, including the 'execution' keyword. For example, 'execution(int + com.foo.TargetObject.countLength(String))' (without the quotes). + + + + + + Access configuration attributes list that applies to all methods matching the pointcut, + e.g. "ROLE_A,ROLE_B" + + + + + + + Allows securing a Message Broker. There are two modes. If no id is specified: ensures that + any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver + registered as a custom argument resolver; ensures that the + SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that + can be manually registered with the clientInboundChannel. + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. If specified, + explicit configuration within clientInboundChannel is required. If not specified, ensures + that any SimpAnnotationMethodMessageHandler has the + AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures + that the SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. + + + + + + Disables the requirement for CSRF token to be present in the Stomp headers (default + false). Changing the default is useful if it is necessary to allow other origins to make + SockJS connections. + + + + + + Use this AuthorizationManager instead of deriving one from <intercept-message> elements + + + + + + Use AuthorizationManager API instead of SecurityMetadatasource (defaults to true) + + + + + + Use this SecurityContextHolderStrategy (note only supported in conjunction with the + AuthorizationManager API) + + + + + + + Creates an authorization rule for a websocket message. + + + + + + + + + + The destination ant pattern which will be mapped to the access attribute. For example, /** + matches any message with a destination, /admin/** matches any message that has a + destination that starts with admin. + + + + + + The access configuration attributes that apply for the configured message. For example, + permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role + 'ROLE_ADMIN'. + + + + + + The type of message to match on. Valid values are defined in SimpMessageType (i.e. + CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, + DISCONNECT_ACK, OTHER). + + + + + + + + + + + + + + + + + + + + Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created + by the namespace. + + + + + + + + + Container element for HTTP security configuration. Multiple elements can now be defined, + each with a specific pattern to which the enclosed security configuration applies. A + pattern can also be configured to bypass Spring Security's filters completely by setting + the "security" attribute to "none". + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + Defines the access-denied strategy that should be used. An access denied page can be + defined or a reference to an AccessDeniedHandler instance. + + + + + + + + + Sets up a form login configuration for authentication with a username and password + + + + + + + + + + + + Configures authentication support for SAML 2.0 Login + + + + + + + + + Configures SAML 2.0 Single Logout support + + + + + + + + + Adds support for X.509 client authentication. + + + + + + + + + + Adds support for basic authentication + + + + + + + + + Incorporates a logout processing filter. Most web applications require a logout filter, + although you may not require one if you write a controller to provider similar logic. + + + + + + + + + + Session-management related functionality is implemented by the addition of a + SessionManagementFilter to the filter stack. + + + + + + + Enables concurrent session control, limiting the number of authenticated sessions a user + may have at the same time. + + + + + + + + + + + + + Sets up remember-me authentication. If used with the "key" attribute (or no attributes) + the cookie-only implementation will be used. Specifying "token-repository-ref" or + "remember-me-data-source-ref" will use the more secure, persisten token approach. + + + + + + + + + Adds support for automatically granting all anonymous web requests a particular principal + identity and a corresponding granted authority. + + + + + + + + + Defines the list of mappings between http and https ports for use in redirects + + + + + + + Provides a method to map http ports to https ports when forcing a redirect. + + + + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + + + + The request URL pattern which will be mapped to the filter chain created by this <http> + element. If omitted, the filter chain will match all requests. + + + + + + When set to 'none', requests matching the pattern attribute will be ignored by Spring + Security. No security filters will be applied and no SecurityContext will be available. If + set, the <http> element must be empty, with no children. + + + + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A legacy attribute which automatically registers a login form, BASIC authentication and a + logout URL and logout services. If unspecified, defaults to "false". We'd recommend you + avoid using this and instead explicitly configure the services you require. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + A reference to a SecurityContextHolderStrategy bean. This can be used to customize how the + SecurityContextHolder is stored during a request + + + + + + Controls the eagerness with which an HTTP session is created by Spring Security classes. + If not set, defaults to "ifRequired". If "stateless" is used, this implies that the + application guarantees that it will not create a session. This differs from the use of + "never" which means that Spring Security will not create a session, but will make use of + one if the application does. + + + + + + + + + + + + + + A reference to a SecurityContextRepository bean. This can be used to customize how the + SecurityContext is stored between requests. + + + + + + Optional attribute that specifies that the SecurityContext should require explicit saving + rather than being synchronized from the SecurityContextHolder. Defaults to "true". + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + Provides versions of HttpServletRequest security methods such as isUserInRole() and + getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to + "true". + + + + + + If available, runs the request as the Subject acquired from the JaasAuthenticationToken. + Defaults to "false". + + + + + + Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true) + + + + + + Use this AuthorizationManager instead of deriving one from <intercept-url> elements + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the realm name that will be used for all authentication + features that require a realm name (eg BASIC and Digest authentication). If unspecified, + defaults to "Spring Security Application". + + + + + + Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + + + + + + Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults + to "false" + + + + + + Corresponds to the shouldFilterAllDispatcherTypes property of AuthorizationFilter. Do not + work when use-authorization-manager=false. Defaults to "true". + + + + + + Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" + (rewriting is disabled). + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + A reference to an AuthenticationManager bean + + + + + + Use this ObservationRegistry to collect metrics on various parts of the filter chain + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + The access configuration attributes that apply for the configured path. + + + + + + The HTTP Method for which the access configuration attributes should apply. If not + specified, the attributes will apply to any method. + + + + + + + + + + + + + + + + + + Used to specify that a URL must be accessed over http or https, or that there is no + preference. The value should be "http", "https" or "any", respectively. + + + + + + The path to the servlet. This attribute is only applicable when 'request-matcher' is + 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are + 2 or more HttpServlet's registered in the ServletContext that have mappings starting with + '/' and are different; 2) The pattern starts with the same value of a registered + HttpServlet path, excluding the default (root) HttpServlet '/'. + + + + + + + + + Specifies the URL that will cause a logout. Spring Security will initialize a filter that + responds to this particular URL. Defaults to /logout if unspecified. + + + + + + Specifies the URL to display once the user has logged out. If not specified, defaults to + <form-login-login-page>/?logout (i.e. /login?logout). + + + + + + Specifies whether a logout also causes HttpSession invalidation, which is generally + desirable. If unspecified, defaults to true. + + + + + + A reference to a LogoutSuccessHandler implementation which will be used to determine the + destination to which the user is taken after logging out. + + + + + + A comma-separated list of the names of cookies which should be deleted when the user logs + out + + + + + + + Allow the RequestCache used for saving requests during the login process to be set + + + + + + + + + + + The URL that the login form is posted to. If unspecified, it defaults to /login. + + + + + + The name of the request parameter which contains the username. Defaults to 'username'. + + + + + + The name of the request parameter which contains the password. Defaults to 'password'. + + + + + + The URL that will be redirected to after successful authentication, if the user's previous + action could not be resumed. This generally happens if the user visits a login page + without having first requested a secured operation that triggers authentication. If + unspecified, defaults to the root of the application. + + + + + + Whether the user should always be redirected to the default-target-url after login. + + + + + + The URL for the login page. If no login URL is specified, Spring Security will + automatically create a login URL at GET /login and a corresponding filter to render that + login URL when requested. + + + + + + The URL for the login failure page. If no login failure URL is specified, Spring Security + will automatically create a failure login URL at /login?error and a corresponding filter + to render that login failure URL when requested. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful authentication request. Should not be used in combination with + default-target-url (or always-use-default-target-url) as the implementation should always + deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationFailureHandler bean which should be used to handle a failed + authentication request. Should not be used in combination with authentication-failure-url + as the implementation should always deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + The URL for the ForwardAuthenticationFailureHandler + + + + + + The URL for the ForwardAuthenticationSuccessHandler + + + + + + + Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the authorization RedirectStrategy + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + Reference to the GrantedAuthoritiesMapper + + + + + + Reference to the OAuth2UserService + + + + + + Reference to the OpenID Connect OAuth2UserService + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + + + + + + + Configures OAuth 2.0 Client support. + + + + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + + Configures OAuth 2.0 Authorization Code Grant. + + + + + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the authorization RedirectStrategy + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + + Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 + Provider. + + + + + + + + + + + + Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the client registration. + + + + + + The client identifier. + + + + + + The client secret. + + + + + + The method used to authenticate the client with the provider. The supported values are + client_secret_basic, client_secret_post and none (public clients). + + + + + + + + + + + + + + + The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The + supported values are authorization_code, client_credentials and password. + + + + + + + + + + + + + The client’s registered redirect URI that the Authorization Server redirects the + end-user’s user-agent to after the end-user has authenticated and authorized access to the + client. + + + + + + A comma-separated list of scope(s) requested by the client during the Authorization + Request flow, such as openid, email, or profile. + + + + + + A descriptive name used for the client. The name may be used in certain scenarios, such as + when displaying the name of the client in the auto-generated login page. + + + + + + A reference to the associated provider. May reference a 'provider' element or use one of + the common providers (google, github, facebook, okta). + + + + + + + The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the provider. + + + + + + The Authorization Endpoint URI for the Authorization Server. + + + + + + The Token Endpoint URI for the Authorization Server. + + + + + + The UserInfo Endpoint URI used to access the claims/attributes of the authenticated + end-user. + + + + + + The authentication method used when sending the access token to the UserInfo Endpoint. The + supported values are header, form and query. + + + + + + + + + + + + + The name of the attribute returned in the UserInfo Response that references the Name or + Identifier of the end-user. + + + + + + The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which + contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID + Token and optionally the UserInfo Response. + + + + + + The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect + 1.0 Provider. + + + + + + + Configures authentication support as an OAuth 2.0 Resource Server. + + + + + + + + + + + + + + Reference to an AuthenticationManagerResolver + + + + + + Reference to a BearerTokenResolver + + + + + + Reference to a AuthenticationEntryPoint + + + + + + + Configures JWT authentication + + + + + + + + + + The URI to use to collect the JWK Set for verifying JWTs + + + + + + Reference to a JwtDecoder + + + + + + Reference to a Converter<Jwt, AbstractAuthenticationToken> + + + + + + + Configuration Opaque Token authentication + + + + + + + + + + The URI to use to introspect opaque token attributes + + + + + + The Client ID to use to authenticate the introspection request + + + + + + The Client secret to use to authenticate the introspection request + + + + + + Reference to an OpaqueTokenIntrospector + + + + + + Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful + introspection result into an Authentication. + + + + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2AuthenticationRequestRepository + + + + + + Reference to the Saml2AuthenticationRequestResolver + + + + + + Reference to the AuthenticationConverter + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the AuthenticationManager + + + + + + + + + The URL by which the relying or asserting party can trigger logout + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Request + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Response + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2LogoutRequestValidator + + + + + + Reference to the Saml2LogoutRequestResolver + + + + + + Reference to the Saml2LogoutRequestRepository + + + + + + Reference to the Saml2LogoutResponseValidator + + + + + + Reference to the Saml2LogoutResponseResolver + + + + + + + Container element for relying party(ies) registered with a SAML 2.0 identity provider + + + + + + + + + + + + + + The identifier by which to refer to the repository in other beans + + + + + + + Represents a relying party registered with a SAML 2.0 identity provider + + + + + + + + + + + + + + The ID that uniquely identifies the relying party registration. + + + + + + The location of the Identity Provider's metadata. + + + + + + The relying party's EntityID + + + + + + The Assertion Consumer Service Location + + + + + + The Assertion Consumer Service Binding + + + + + + A reference to the associated asserting party. + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Location</a> + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Response Location</a> + + + + + + The relying party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Binding</a> + + + + + + + The relying party's signing credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The relying party's decryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The configuration metadata of the Asserting party + + + + + + + + + + + + + + A unique identifier of the asserting party. + + + + + + The asserting party's EntityID. + + + + + + Indicates the asserting party's preference that relying parties should sign the + AuthnRequest before sending + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Location. + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Binding. + + + + + + A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this + asserting party, in preference order. + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Location</a> + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Response Location</a> + + + + + + The asserting party <a + href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService + Binding</a> + + + + + + + The relying party's verification credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The asserting party's encryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + + + + + + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + Used within to define a specific URL pattern and the list of filters which apply to the + URLs matching that pattern. When multiple filter-chain elements are assembled in a list in + order to configure a FilterChainProxy, the most specific patterns must be placed at the + top of the list, with most general ones at the bottom. + + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A comma separated list of bean names that implement Filter that should be processed for + this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + + Used to explicitly configure a FilterSecurityMetadataSource bean for use with a + FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy + explicitly, rather than using the <http> element. The intercept-url elements used should + only contain pattern, method and access attributes. Any others will result in a + configuration error. + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + + Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds support for the password management. + + + + + + + + + + The change password page. Defaults to "/change-password". + + + + + + + + + Specifies that SessionAuthenticationStrategy must be explicitly invoked. Default false + (i.e. SessionManagementFilter will implicitly invoke SessionAuthenticationStrategy). + + + + + + Indicates how session fixation protection will be applied when a user authenticates. If + set to "none", no protection will be applied. "newSession" will create a new empty + session, with only Spring Security-related attributes migrated. "migrateSession" will + create a new session and copy all session attributes to the new session. In Servlet 3.1 + (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing + session and use the container-supplied session fixation protection + (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and + newer containers, "migrateSession" in older containers. Throws an exception if + "changeSessionId" is used in older containers. + + + + + + + + + + + + + + The URL to which a user will be redirected if they submit an invalid session indentifier. + Typically used to detect session timeouts. + + + + + + Allows injection of the InvalidSessionStrategy instance used by the + SessionManagementFilter + + + + + + Allows injection of the SessionAuthenticationStrategy instance used by the + SessionManagementFilter + + + + + + Defines the URL of the error page which should be shown when the + SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error + code will be returned to the client. Note that this attribute doesn't apply if the error + occurs during a form-based login, where the URL for authentication failure will take + precedence. + + + + + + + + + The maximum number of sessions a single authenticated user can have open at the same time. + Defaults to "1". A negative value denotes unlimited sessions. + + + + + + The URL a user will be redirected to if they attempt to use a session which has been + "expired" because they have logged in again. + + + + + + Allows injection of the SessionInformationExpiredStrategy instance used by the + ConcurrentSessionFilter + + + + + + Specifies that an unauthorized error should be reported when a user attempts to login when + they already have the maximum configured sessions open. The default behaviour is to expire + the original session. If the session-authentication-error-url attribute is set on the + session-management URL, the user will be redirected to this URL. + + + + + + Allows you to define an alias for the SessionRegistry bean in order to access it in your + own configuration. + + + + + + Allows you to define an external SessionRegistry bean to be used by the concurrency + control setup. + + + + + + + + + The "key" used to identify cookies from a specific token-based remember-me application. + You should set this to a unique value for your application. If unset, it will default to a + random value generated by SecureRandom. + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + A reference to a DataSource bean + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Exports the internally defined RememberMeServices as a bean alias, allowing it to be used + by other beans in the application context. + + + + + + Determines whether the "secure" flag will be set on the remember-me cookie. If set to + true, the cookie will only be submitted over HTTPS (recommended). By default, secure + cookies will be used if the request is made on a secure connection. + + + + + + The period (in seconds) for which the remember-me cookie should be valid. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful remember-me authentication. + + + + + + The name of the request parameter which toggles remember-me authentication. Defaults to + 'remember-me'. + + + + + + The name of cookie which store the token for remember-me authentication. Defaults to + 'remember-me'. + + + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + + + Allows a custom implementation of RememberMeServices to be used. Note that this + implementation should return RememberMeAuthenticationToken instances with the same "key" + value as specified in the remember-me element. Alternatively it should register its own + AuthenticationProvider. It should also implement the LogoutHandler interface, which will + be invoked when a user logs out. Typically the remember-me cookie would be removed on + logout. + + + + + + + + + + + + The key shared between the provider and filter. This generally does not need to be set. If + unset, it will default to a random value generated by SecureRandom. + + + + + + The username that should be assigned to the anonymous request. This allows the principal + to be identified, which may be important for logging and auditing. if unset, defaults to + "anonymousUser". + + + + + + The granted authority that should be assigned to the anonymous request. Commonly this is + used to assign the anonymous request particular roles, which can subsequently be used in + authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + + + + + + With the default namespace setup, the anonymous "authentication" facility is automatically + enabled. You can disable it using this property. + + + + + + + + + + The http port to use. + + + + + + + + The https port to use. + + + + + + + + + The regular expression used to obtain the username from the certificate's subject. + Defaults to matching on the common name using the pattern "CN=(.*?),". + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration + with container authentication. + + + + + + + + + + A comma-separate list of roles to look for in the incoming HttpServletRequest. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Registers the AuthenticationManager instance and allows its list of + AuthenticationProviders to be defined. Also allows you to define an alias to allow you to + reference the AuthenticationManager in your own beans. + + + + + + + Indicates that the contained user-service should be used as an authentication source. + + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + Sets up an ldap authentication provider + + + + + + + Specifies that an LDAP provider should use an LDAP compare operation of the user's + password to authenticate the user + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + An alias you wish to use for the AuthenticationManager bean (not required it you are using + a specific id) + + + + + + If set to true, the AuthenticationManger will attempt to clear any credentials data in the + returned Authentication object, once the user has been authenticated. + + + + + + Use this ObservationRegistry to collect metrics on various parts of the filter chain + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Creates an in-memory UserDetailsService from a properties file or a list of "user" child + elements. Usernames are converted to lower-case internally to allow for case-insensitive + lookups, so this should not be used if case-sensitivity is required. + + + + + + + Represents a user in the application. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The location of a Properties file where each line is in the format of + username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + + + + + + + + + The username assigned to the user. + + + + + + The password assigned to the user. This may be hashed if the corresponding authentication + provider supports hashing (remember to set the "hash" attribute of the "user-service" + element). This attribute be omitted in the case where the data will not be used for + authentication, but only for accessing authorities. If omitted, the namespace will + generate a random value, preventing its accidental use for authentication. Cannot be + empty. + + + + + + One of more authorities granted to the user. Separate authorities with a comma (but no + space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + + + + + Can be set to "true" to mark an account as locked and unusable. + + + + + + Can be set to "true" to mark an account as disabled and unusable. + + + + + + + Causes creation of a JDBC-based UserDetailsService. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The bean ID of the DataSource which provides the required tables. + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + An SQL statement to query a username, password, and enabled status given a username. + Default is "select username,password,enabled from users where username = ?" + + + + + + An SQL statement to query for a user's granted authorities given a username. The default + is "select username, authority from authorities where username = ?" + + + + + + An SQL statement to query user's group authorities given a username. The default is + "select g.id, g.group_name, ga.authority from groups g, group_members gm, + group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + Element for configuration of the CsrfFilter for protection against CSRF. It also updates + the default RequestCache to only replay "GET" requests. + + + + + + + + + + Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is + enabled). + + + + + + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is + any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + + + + + + The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by + LazyCsrfTokenRepository. + + + + + + The CsrfTokenRequestHandler to use. The default is CsrfTokenRequestAttributeHandler. + + + + + + + Element for configuration of the HeaderWritersFilter. Enables easy setting for the + X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies if the default headers should be disabled. Default false. + + + + + + Specifies if headers should be disabled. Default false. + + + + + + + Adds support for HTTP Strict Transport Security (HSTS) + + + + + + + + + + Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default true. + + + + + + Specifies the maximum amount of time the host should be considered a Known HSTS Host. + Default one year. + + + + + + The RequestMatcher instance to be used to determine if the header should be set. Default + is if HttpServletRequest.isSecure() is true. + + + + + + Specifies if preload should be included. Default false. + + + + + + + Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is + specified a HandlerMappingIntrospector is used as the CorsConfigurationSource + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to + use + + + + + + + Adds support for HTTP Public Key Pinning (HPKP). + + + + + + + + + + + + + + + + + + The list with pins + + + + + + + + + + + A pin is specified using the base64-encoded SPKI fingerprint as value and the + cryptographic hash algorithm as attribute + + + + + + The cryptographic hash algorithm + + + + + + + + + Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default false. + + + + + + Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + + + + + + Specifies if the browser should only report pin validation failures. Default true. + + + + + + Specifies the URI to which the browser should report pin validation failures. + + + + + + + Adds support for Content Security Policy (CSP) + + + + + + + + + + The security policy directive(s) for the Content-Security-Policy header or if report-only + is set to true, then the Content-Security-Policy-Report-Only header is used. + + + + + + Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy + violations only. Defaults to false. + + + + + + + Adds support for Referrer Policy + + + + + + + + + + The policies for the Referrer-Policy header. + + + + + + + + + + + + + + + + + + + Adds support for Feature Policy + + + + + + + + + + The security policy directive(s) for the Feature-Policy header. + + + + + + + Adds support for Permissions Policy + + + + + + + + + + The policies for the Permissions-Policy header. + + + + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for + every request + + + + + + + + + + Specifies if Cache Control should be disabled. Default false. + + + + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options + header. + + + + + + + + + + If disabled, the X-Frame-Options header will not be included. Default false. + + + + + + Specify the policy to use for the X-Frame-Options-Header. + + + + + + + + + + + + + Specify the strategy to use when ALLOW-FROM is chosen. + + + + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specify a value to use for the chosen strategy. + + + + + + Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' + based strategy. Default is 'from'. Deprecated ALLOW-FROM is an obsolete directive that no + longer works in modern browsers. Instead use Content-Security-Policy with the <a + href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors">frame-ancestors</a> + directive. + + + + + + + Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the + X-XSS-Protection header. + + + + + + + + + + disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + + + + + + Specify the value for the X-Xss-Protection header. Defaults to "0". + + + + + + + + + + + + + + Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + + + + + + + + + + If disabled, the X-Content-Type-Options header will not be included. Default false. + + + + + + + Adds support for Cross-Origin-Opener-Policy header + + + + + + + + + + The policies for the Cross-Origin-Opener-Policy header. + + + + + + + + + + + + + + Adds support for Cross-Origin-Embedder-Policy header + + + + + + + + + + The policies for the Cross-Origin-Embedder-Policy header. + + + + + + + + + + + + + Adds support for Cross-Origin-Resource-Policy header + + + + + + + + + + The policies for the Cross-Origin-Resource-Policy header. + + + + + + + + + + + + + + Add additional headers to the response. + + + + + + + + + + The name of the header to add. + + + + + + The value for the header. + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Used to indicate that a filter bean declaration should be incorporated into the security + filter chain. + + + + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java b/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java index 0773010b82c..2982d2a005a 100644 --- a/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java +++ b/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,52 +25,124 @@ import java.io.ObjectOutputStream; import java.io.ObjectStreamClass; import java.io.Serializable; +import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.http.Cookie; import org.apereo.cas.client.validation.AssertionImpl; import org.instancio.Instancio; import org.instancio.InstancioApi; +import org.instancio.InstancioOfClassApi; import org.instancio.Select; import org.instancio.generator.Generator; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.ResolvableType; import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.AuthorizationServiceException; +import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.intercept.RunAsUserToken; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.ProviderNotFoundException; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; +import org.springframework.security.authentication.event.AuthenticationFailureCredentialsExpiredEvent; +import org.springframework.security.authentication.event.AuthenticationFailureDisabledEvent; +import org.springframework.security.authentication.event.AuthenticationFailureExpiredEvent; +import org.springframework.security.authentication.event.AuthenticationFailureLockedEvent; +import org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent; +import org.springframework.security.authentication.event.AuthenticationFailureProxyUntrustedEvent; +import org.springframework.security.authentication.event.AuthenticationFailureServiceExceptionEvent; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; +import org.springframework.security.authentication.event.LogoutSuccessEvent; import org.springframework.security.authentication.jaas.JaasAuthenticationToken; +import org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent; +import org.springframework.security.authentication.jaas.event.JaasAuthenticationSuccessEvent; +import org.springframework.security.authentication.ott.InvalidOneTimeTokenException; +import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken; +import org.springframework.security.authentication.password.CompromisedPasswordException; +import org.springframework.security.authorization.AuthorityAuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; import org.springframework.security.cas.authentication.CasAuthenticationToken; import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.context.TransientSecurityContext; +import org.springframework.security.core.session.AbstractSessionEvent; import org.springframework.security.core.session.ReactiveSessionInformation; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyErrorStatus; +import org.springframework.security.ldap.ppolicy.PasswordPolicyException; +import org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl; +import org.springframework.security.ldap.userdetails.LdapAuthority; +import org.springframework.security.oauth2.client.ClientAuthorizationException; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens; import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens; +import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; +import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens; +import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; +import org.springframework.security.oauth2.client.oidc.session.TestOidcSessionInformations; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2DeviceCode; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2UserCode; import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; @@ -79,16 +151,95 @@ import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.oauth2.core.user.TestOAuth2Users; +import org.springframework.security.oauth2.jwt.BadJwtException; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoderInitializationException; +import org.springframework.security.oauth2.jwt.JwtEncodingException; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.JwtValidationException; import org.springframework.security.oauth2.jwt.TestJwts; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrors; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.credentials.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationTokens; +import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications; +import org.springframework.security.saml2.provider.service.authentication.TestSaml2PostAuthenticationRequests; +import org.springframework.security.saml2.provider.service.authentication.TestSaml2RedirectAuthenticationRequests; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; +import org.springframework.security.web.PortResolverImpl; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; +import org.springframework.security.web.authentication.rememberme.CookieTheftException; +import org.springframework.security.web.authentication.rememberme.InvalidCookieException; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionFixationProtectionEvent; +import org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent; +import org.springframework.security.web.authentication.www.NonceExpiredException; +import org.springframework.security.web.csrf.CsrfException; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.security.web.csrf.InvalidCsrfTokenException; +import org.springframework.security.web.csrf.MissingCsrfTokenException; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.security.web.savedrequest.DefaultSavedRequest; +import org.springframework.security.web.savedrequest.SimpleSavedRequest; +import org.springframework.security.web.server.firewall.ServerExchangeRejectedException; +import org.springframework.security.web.session.HttpSessionCreatedEvent; +import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs; +import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs; +import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse; +import org.springframework.security.web.webauthn.api.AuthenticatorAttachment; +import org.springframework.security.web.webauthn.api.AuthenticatorTransport; +import org.springframework.security.web.webauthn.api.Bytes; +import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput; +import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput; +import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInput; +import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInputs; +import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.api.PublicKeyCredential; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialType; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses; +import org.springframework.security.web.webauthn.api.TestBytes; +import org.springframework.security.web.webauthn.api.TestPublicKeyCredential; +import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions; +import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.api.UserVerificationRequirement; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationRequestToken; +import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -118,23 +269,16 @@ class SpringSecurityCoreVersionSerializableTests { static Path previousVersionFolder = Paths.get("src/test/resources/serialized/" + getPreviousVersion()); static { - ClientRegistration.Builder clientRegistrationBuilder = TestClientRegistrations.clientRegistration(); - ClientRegistration clientRegistration = clientRegistrationBuilder.build(); UserDetails user = TestAuthentication.user(); - WebAuthenticationDetails details = new WebAuthenticationDetails("remote", "sessionId"); + Authentication authentication = TestAuthentication.authenticated(user); + SecurityContext securityContext = new SecurityContextImpl(authentication); + + // oauth2-core generatorByClassName.put(DefaultOAuth2User.class, (r) -> TestOAuth2Users.create()); - generatorByClassName.put(ClientRegistration.class, (r) -> clientRegistration); - generatorByClassName.put(ClientRegistration.ProviderDetails.class, - (r) -> clientRegistration.getProviderDetails()); - generatorByClassName.put(ClientRegistration.ProviderDetails.UserInfoEndpoint.class, - (r) -> clientRegistration.getProviderDetails().getUserInfoEndpoint()); - generatorByClassName.put(ClientRegistration.Builder.class, (r) -> clientRegistrationBuilder); generatorByClassName.put(OAuth2AuthorizationRequest.class, (r) -> TestOAuth2AuthorizationRequests.request().build()); generatorByClassName.put(OAuth2AuthorizationResponse.class, (r) -> TestOAuth2AuthorizationResponses.success().build()); - generatorByClassName.put(OAuth2AuthorizedClient.class, - (r) -> new OAuth2AuthorizedClient(clientRegistration, "principal", TestOAuth2AccessTokens.noScopes())); generatorByClassName.put(OAuth2UserAuthority.class, (r) -> new OAuth2UserAuthority(Map.of("username", "user"))); generatorByClassName.put(OAuth2AuthorizationExchange.class, (r) -> TestOAuth2AuthorizationExchanges.success()); generatorByClassName.put(OidcUserInfo.class, (r) -> OidcUserInfo.builder().email("email@example.com").build()); @@ -142,6 +286,36 @@ class SpringSecurityCoreVersionSerializableTests { (r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L))); generatorByClassName.put(ReactiveSessionInformation.class, (r) -> new ReactiveSessionInformation(user, r.alphanumeric(4), Instant.ofEpochMilli(1704378933936L))); + generatorByClassName.put(OAuth2AccessToken.class, (r) -> TestOAuth2AccessTokens.scopes("scope")); + generatorByClassName.put(OAuth2DeviceCode.class, + (r) -> new OAuth2DeviceCode("token", Instant.now(), Instant.now())); + generatorByClassName.put(OAuth2RefreshToken.class, + (r) -> new OAuth2RefreshToken("refreshToken", Instant.now(), Instant.now())); + generatorByClassName.put(OAuth2UserCode.class, + (r) -> new OAuth2UserCode("token", Instant.now(), Instant.now())); + generatorByClassName.put(DefaultOidcUser.class, (r) -> TestOidcUsers.create()); + generatorByClassName.put(OidcUserAuthority.class, + (r) -> new OidcUserAuthority(TestOidcIdTokens.idToken().build(), + new OidcUserInfo(Map.of("claim", "value")), "claim")); + generatorByClassName.put(OAuth2AuthenticationException.class, + (r) -> new OAuth2AuthenticationException(new OAuth2Error("error", "description", "uri"), "message", + new RuntimeException())); + generatorByClassName.put(OAuth2AuthorizationException.class, + (r) -> new OAuth2AuthorizationException(new OAuth2Error("error", "description", "uri"), "message", + new RuntimeException())); + + // oauth2-client + ClientRegistration.Builder clientRegistrationBuilder = TestClientRegistrations.clientRegistration(); + ClientRegistration clientRegistration = clientRegistrationBuilder.build(); + WebAuthenticationDetails details = new WebAuthenticationDetails("remote", "sessionId"); + generatorByClassName.put(ClientRegistration.class, (r) -> clientRegistration); + generatorByClassName.put(ClientRegistration.ProviderDetails.class, + (r) -> clientRegistration.getProviderDetails()); + generatorByClassName.put(ClientRegistration.ProviderDetails.UserInfoEndpoint.class, + (r) -> clientRegistration.getProviderDetails().getUserInfoEndpoint()); + generatorByClassName.put(ClientRegistration.Builder.class, (r) -> clientRegistrationBuilder); + generatorByClassName.put(OAuth2AuthorizedClient.class, + (r) -> new OAuth2AuthorizedClient(clientRegistration, "principal", TestOAuth2AccessTokens.noScopes())); generatorByClassName.put(OAuth2LoginAuthenticationToken.class, (r) -> { var token = new OAuth2LoginAuthenticationToken(clientRegistration, TestOAuth2AuthorizationExchanges.success()); @@ -153,6 +327,40 @@ class SpringSecurityCoreVersionSerializableTests { token.setDetails(details); return token; }); + generatorByClassName.put(OAuth2AuthenticationToken.class, (r) -> { + var token = TestOAuth2AuthenticationTokens.authenticated(); + token.setDetails(details); + return token; + }); + generatorByClassName.put(OidcIdToken.class, (r) -> TestOidcIdTokens.idToken().build()); + generatorByClassName.put(OidcLogoutToken.class, + (r) -> TestOidcLogoutTokens.withSessionId("issuer", "sessionId").issuedAt(Instant.now()).build()); + generatorByClassName.put(OidcSessionInformation.class, (r) -> TestOidcSessionInformations.create()); + generatorByClassName.put(DefaultOAuth2AuthenticatedPrincipal.class, (r) -> { + OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active(); + return new DefaultOAuth2AuthenticatedPrincipal(principal.getName(), principal.getAttributes(), + (Collection) principal.getAuthorities()); + }); + generatorByClassName.put(ClientAuthorizationException.class, + (r) -> new ClientAuthorizationException(new OAuth2Error("error", "description", "uri"), "id", "message", + new RuntimeException())); + generatorByClassName.put(ClientAuthorizationRequiredException.class, + (r) -> new ClientAuthorizationRequiredException("id")); + + // oauth2-jose + generatorByClassName.put(BadJwtException.class, (r) -> new BadJwtException("token", new RuntimeException())); + generatorByClassName.put(JwtDecoderInitializationException.class, + (r) -> new JwtDecoderInitializationException("message", new RuntimeException())); + generatorByClassName.put(JwtEncodingException.class, + (r) -> new JwtEncodingException("message", new RuntimeException())); + generatorByClassName.put(JwtException.class, (r) -> new JwtException("message", new RuntimeException())); + generatorByClassName.put(JwtValidationException.class, + (r) -> new JwtValidationException("message", List.of(new OAuth2Error("error", "description", "uri")))); + + // oauth2-jwt + generatorByClassName.put(Jwt.class, (r) -> TestJwts.user()); + + // oauth2-resource-server generatorByClassName .put(org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken.class, (r) -> { var token = new org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken( @@ -171,22 +379,114 @@ class SpringSecurityCoreVersionSerializableTests { token.setDetails(details); return token; }); - generatorByClassName.put(OAuth2AuthenticationToken.class, (r) -> { - var token = TestOAuth2AuthenticationTokens.authenticated(); - token.setDetails(details); - return token; - }); generatorByClassName.put(JwtAuthenticationToken.class, (r) -> { var token = new JwtAuthenticationToken(TestJwts.user()); token.setDetails(details); return token; }); + generatorByClassName.put(BearerTokenError.class, (r) -> BearerTokenErrors.invalidToken("invalid token")); + generatorByClassName.put(OAuth2IntrospectionAuthenticatedPrincipal.class, + (r) -> TestOAuth2AuthenticatedPrincipals.active()); + generatorByClassName.put(InvalidBearerTokenException.class, + (r) -> new InvalidBearerTokenException("description", new RuntimeException())); + generatorByClassName.put(BadOpaqueTokenException.class, + (r) -> new BadOpaqueTokenException("message", new RuntimeException())); + generatorByClassName.put(OAuth2IntrospectionException.class, + (r) -> new OAuth2IntrospectionException("message", new RuntimeException())); + + // core generatorByClassName.put(RunAsUserToken.class, (r) -> { RunAsUserToken token = new RunAsUserToken("key", user, "creds", user.getAuthorities(), AnonymousAuthenticationToken.class); token.setDetails(details); return token; }); + generatorByClassName.put(RememberMeAuthenticationToken.class, (r) -> { + RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key", user, user.getAuthorities()); + token.setDetails(details); + return token; + }); + generatorByClassName.put(UsernamePasswordAuthenticationToken.class, (r) -> { + var token = UsernamePasswordAuthenticationToken.unauthenticated(user, "creds"); + token.setDetails(details); + return token; + }); + generatorByClassName.put(JaasAuthenticationToken.class, (r) -> { + var token = new JaasAuthenticationToken(user, "creds", null); + token.setDetails(details); + return token; + }); + generatorByClassName.put(OneTimeTokenAuthenticationToken.class, + (r) -> applyDetails(new OneTimeTokenAuthenticationToken("username", "token"))); + generatorByClassName.put(AccessDeniedException.class, + (r) -> new AccessDeniedException("access denied", new RuntimeException())); + generatorByClassName.put(AuthorizationServiceException.class, + (r) -> new AuthorizationServiceException("access denied", new RuntimeException())); + generatorByClassName.put(AccountExpiredException.class, + (r) -> new AccountExpiredException("error", new RuntimeException())); + generatorByClassName.put(AuthenticationCredentialsNotFoundException.class, + (r) -> new AuthenticationCredentialsNotFoundException("error", new RuntimeException())); + generatorByClassName.put(AuthenticationServiceException.class, + (r) -> new AuthenticationServiceException("error", new RuntimeException())); + generatorByClassName.put(BadCredentialsException.class, + (r) -> new BadCredentialsException("error", new RuntimeException())); + generatorByClassName.put(CredentialsExpiredException.class, + (r) -> new CredentialsExpiredException("error", new RuntimeException())); + generatorByClassName.put(DisabledException.class, + (r) -> new DisabledException("error", new RuntimeException())); + generatorByClassName.put(InsufficientAuthenticationException.class, + (r) -> new InsufficientAuthenticationException("error", new RuntimeException())); + generatorByClassName.put(InternalAuthenticationServiceException.class, + (r) -> new InternalAuthenticationServiceException("error", new RuntimeException())); + generatorByClassName.put(LockedException.class, (r) -> new LockedException("error", new RuntimeException())); + generatorByClassName.put(ProviderNotFoundException.class, (r) -> new ProviderNotFoundException("error")); + generatorByClassName.put(InvalidOneTimeTokenException.class, (r) -> new InvalidOneTimeTokenException("error")); + generatorByClassName.put(CompromisedPasswordException.class, + (r) -> new CompromisedPasswordException("error", new RuntimeException())); + generatorByClassName.put(UsernameNotFoundException.class, + (r) -> new UsernameNotFoundException("error", new RuntimeException())); + generatorByClassName.put(TestingAuthenticationToken.class, + (r) -> applyDetails(new TestingAuthenticationToken("username", "password"))); + generatorByClassName.put(AuthenticationFailureBadCredentialsEvent.class, + (r) -> new AuthenticationFailureBadCredentialsEvent(authentication, + new BadCredentialsException("message"))); + generatorByClassName.put(AuthenticationFailureCredentialsExpiredEvent.class, + (r) -> new AuthenticationFailureCredentialsExpiredEvent(authentication, + new CredentialsExpiredException("message"))); + generatorByClassName.put(AuthenticationFailureDisabledEvent.class, + (r) -> new AuthenticationFailureDisabledEvent(authentication, new DisabledException("message"))); + generatorByClassName.put(AuthenticationFailureExpiredEvent.class, + (r) -> new AuthenticationFailureExpiredEvent(authentication, new AccountExpiredException("message"))); + generatorByClassName.put(AuthenticationFailureLockedEvent.class, + (r) -> new AuthenticationFailureLockedEvent(authentication, new LockedException("message"))); + generatorByClassName.put(AuthenticationFailureProviderNotFoundEvent.class, + (r) -> new AuthenticationFailureProviderNotFoundEvent(authentication, + new ProviderNotFoundException("message"))); + generatorByClassName.put(AuthenticationFailureProxyUntrustedEvent.class, + (r) -> new AuthenticationFailureProxyUntrustedEvent(authentication, + new AuthenticationServiceException("message"))); + generatorByClassName.put(AuthenticationFailureServiceExceptionEvent.class, + (r) -> new AuthenticationFailureServiceExceptionEvent(authentication, + new AuthenticationServiceException("message"))); + generatorByClassName.put(AuthenticationSuccessEvent.class, + (r) -> new AuthenticationSuccessEvent(authentication)); + generatorByClassName.put(InteractiveAuthenticationSuccessEvent.class, + (r) -> new InteractiveAuthenticationSuccessEvent(authentication, Authentication.class)); + generatorByClassName.put(LogoutSuccessEvent.class, (r) -> new LogoutSuccessEvent(authentication)); + generatorByClassName.put(JaasAuthenticationFailedEvent.class, + (r) -> new JaasAuthenticationFailedEvent(authentication, new RuntimeException("message"))); + generatorByClassName.put(JaasAuthenticationSuccessEvent.class, + (r) -> new JaasAuthenticationSuccessEvent(authentication)); + generatorByClassName.put(AbstractSessionEvent.class, (r) -> new AbstractSessionEvent(securityContext)); + generatorByClassName.put(SecurityConfig.class, (r) -> new SecurityConfig("value")); + generatorByClassName.put(TransientSecurityContext.class, (r) -> new TransientSecurityContext(authentication)); + generatorByClassName.put(AuthorizationDeniedException.class, + (r) -> new AuthorizationDeniedException("message", new AuthorizationDecision(false))); + generatorByClassName.put(AuthorizationDecision.class, (r) -> new AuthorizationDecision(true)); + generatorByClassName.put(AuthorityAuthorizationDecision.class, + (r) -> new AuthorityAuthorizationDecision(true, AuthorityUtils.createAuthorityList("ROLE_USER"))); + + // cas generatorByClassName.put(CasServiceTicketAuthenticationToken.class, (r) -> { CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("creds"); token.setDetails(details); @@ -203,27 +503,166 @@ class SpringSecurityCoreVersionSerializableTests { token.setDetails(details); return token; }); - generatorByClassName.put(RememberMeAuthenticationToken.class, (r) -> { - RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key", user, user.getAuthorities()); + + // ldap + generatorByClassName.put(LdapAuthority.class, + (r) -> new LdapAuthority("USER", "username", Map.of("attribute", List.of("value1", "value2")))); + generatorByClassName.put(PasswordPolicyException.class, + (r) -> new PasswordPolicyException(PasswordPolicyErrorStatus.INSUFFICIENT_PASSWORD_QUALITY)); + generatorByClassName.put(PasswordPolicyControl.class, (r) -> new PasswordPolicyControl(true)); + generatorByClassName.put(PasswordPolicyResponseControl.class, (r) -> { + byte[] encodedResponse = { 0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA0, 0x1, 0x21 }; + return new PasswordPolicyResponseControl(encodedResponse); + }); + + // saml2-service-provider + generatorByClassName.put(Saml2AuthenticationException.class, + (r) -> new Saml2AuthenticationException(new Saml2Error("code", "descirption"), "message", + new IOException("fail"))); + generatorByClassName.put(Saml2Exception.class, (r) -> new Saml2Exception("message", new IOException("fail"))); + generatorByClassName.put(DefaultSaml2AuthenticatedPrincipal.class, + (r) -> TestSaml2Authentications.authentication().getPrincipal()); + generatorByClassName.put(Saml2Authentication.class, + (r) -> applyDetails(TestSaml2Authentications.authentication())); + generatorByClassName.put(Saml2PostAuthenticationRequest.class, + (r) -> TestSaml2PostAuthenticationRequests.create()); + generatorByClassName.put(Saml2RedirectAuthenticationRequest.class, + (r) -> TestSaml2RedirectAuthenticationRequests.create()); + generatorByClassName.put(Saml2X509Credential.class, + (r) -> TestSaml2X509Credentials.relyingPartyVerifyingCredential()); + generatorByClassName.put(AssertingPartyDetails.class, + (r) -> TestRelyingPartyRegistrations.full().build().getAssertingPartyMetadata()); + generatorByClassName.put(RelyingPartyRegistration.class, (r) -> TestRelyingPartyRegistrations.full().build()); + generatorByClassName.put(Saml2AuthenticationToken.class, (r) -> { + Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.tokenRequested(); token.setDetails(details); return token; }); + + // web + generatorByClassName.put(AnonymousAuthenticationToken.class, (r) -> { + Collection authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + return applyDetails(new AnonymousAuthenticationToken("key", "username", authorities)); + }); generatorByClassName.put(PreAuthenticatedAuthenticationToken.class, (r) -> { PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(user, "creds", user.getAuthorities()); token.setDetails(details); return token; }); - generatorByClassName.put(UsernamePasswordAuthenticationToken.class, (r) -> { - var token = UsernamePasswordAuthenticationToken.unauthenticated(user, "creds"); - token.setDetails(details); - return token; + generatorByClassName.put(PreAuthenticatedCredentialsNotFoundException.class, + (r) -> new PreAuthenticatedCredentialsNotFoundException("message", new IOException("fail"))); + generatorByClassName.put(CookieTheftException.class, (r) -> new CookieTheftException("message")); + generatorByClassName.put(InvalidCookieException.class, (r) -> new InvalidCookieException("message")); + generatorByClassName.put(RememberMeAuthenticationException.class, + (r) -> new RememberMeAuthenticationException("message", new IOException("fail"))); + generatorByClassName.put(SessionAuthenticationException.class, + (r) -> new SessionAuthenticationException("message")); + generatorByClassName.put(NonceExpiredException.class, + (r) -> new NonceExpiredException("message", new IOException("fail"))); + generatorByClassName.put(CsrfException.class, (r) -> new CsrfException("message")); + generatorByClassName.put(org.springframework.security.web.server.csrf.CsrfException.class, + (r) -> new org.springframework.security.web.server.csrf.CsrfException("message")); + generatorByClassName.put(InvalidCsrfTokenException.class, + (r) -> new InvalidCsrfTokenException(new DefaultCsrfToken("header", "parameter", "token"), "token")); + generatorByClassName.put(MissingCsrfTokenException.class, (r) -> new MissingCsrfTokenException("token")); + generatorByClassName.put(DefaultCsrfToken.class, (r) -> new DefaultCsrfToken("header", "parameter", "token")); + generatorByClassName.put(org.springframework.security.web.server.csrf.DefaultCsrfToken.class, + (r) -> new org.springframework.security.web.server.csrf.DefaultCsrfToken("header", "parameter", + "token")); + generatorByClassName.put(RequestRejectedException.class, (r) -> new RequestRejectedException("message")); + generatorByClassName.put(ServerExchangeRejectedException.class, + (r) -> new ServerExchangeRejectedException("message")); + generatorByClassName.put(SessionFixationProtectionEvent.class, + (r) -> new SessionFixationProtectionEvent(authentication, "old", "new")); + generatorByClassName.put(AuthenticationSwitchUserEvent.class, + (r) -> new AuthenticationSwitchUserEvent(authentication, user)); + generatorByClassName.put(HttpSessionCreatedEvent.class, + (r) -> new HttpSessionCreatedEvent(new MockHttpSession())); + generatorByClassName.put(SimpleSavedRequest.class, (r) -> { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/uri"); + request.setQueryString("query=string"); + request.setScheme("https"); + request.setServerName("localhost"); + request.setServerPort(80); + request.setRequestURI("/uri"); + request.setCookies(new Cookie("name", "value")); + request.addHeader("header", "value"); + request.addParameter("parameter", "value"); + request.setPathInfo("/path"); + request.addPreferredLocale(Locale.ENGLISH); + return new SimpleSavedRequest(new DefaultSavedRequest(request, new PortResolverImpl(), "continue")); }); - generatorByClassName.put(JaasAuthenticationToken.class, (r) -> { - var token = new JaasAuthenticationToken(user, "creds", null); - token.setDetails(details); - return token; + + // webauthn + generatorByClassName.put(Bytes.class, (r) -> TestBytes.get()); + generatorByClassName.put(ImmutablePublicKeyCredentialUserEntity.class, + (r) -> TestPublicKeyCredentialUserEntity.userEntity().id(TestBytes.get()).build()); + generatorByClassName.put(WebAuthnAuthentication.class, (r) -> { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity() + .id(TestBytes.get()) + .build(); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + WebAuthnAuthentication webAuthnAuthentication = new WebAuthnAuthentication(userEntity, authorities); + webAuthnAuthentication.setDetails(details); + return webAuthnAuthentication; }); + + // webauthn + CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = new CredProtectAuthenticationExtensionsClientInput.CredProtect( + CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy.USER_VERIFICATION_OPTIONAL, + true); + Bytes id = TestBytes.get(); + AuthenticationExtensionsClientInputs inputs = new ImmutableAuthenticationExtensionsClientInputs( + ImmutableAuthenticationExtensionsClientInput.credProps); + // @formatter:off + PublicKeyCredentialDescriptor descriptor = PublicKeyCredentialDescriptor.builder() + .id(id) + .type(PublicKeyCredentialType.PUBLIC_KEY) + .transports(Set.of(AuthenticatorTransport.USB)) + .build(); + // @formatter:on + generatorByClassName.put(AuthenticatorTransport.class, (a) -> AuthenticatorTransport.USB); + generatorByClassName.put(PublicKeyCredentialType.class, (k) -> PublicKeyCredentialType.PUBLIC_KEY); + generatorByClassName.put(UserVerificationRequirement.class, (r) -> UserVerificationRequirement.REQUIRED); + generatorByClassName.put(CredProtectAuthenticationExtensionsClientInput.CredProtect.class, (c) -> credProtect); + generatorByClassName.put(CredProtectAuthenticationExtensionsClientInput.class, + (c) -> new CredProtectAuthenticationExtensionsClientInput(credProtect)); + generatorByClassName.put(ImmutableAuthenticationExtensionsClientInputs.class, (i) -> inputs); + Field credPropsField = ReflectionUtils.findField(ImmutableAuthenticationExtensionsClientInput.class, + "credProps"); + generatorByClassName.put(credPropsField.getType(), + (i) -> ImmutableAuthenticationExtensionsClientInput.credProps); + generatorByClassName.put(Bytes.class, (b) -> id); + generatorByClassName.put(PublicKeyCredentialDescriptor.class, (d) -> descriptor); + // @formatter:off + generatorByClassName.put(PublicKeyCredentialRequestOptions.class, (o) -> TestPublicKeyCredentialRequestOptions.create() + .extensions(inputs) + .allowCredentials(List.of(descriptor)) + .build() + ); + + CredentialPropertiesOutput credentialOutput = new CredentialPropertiesOutput(false); + AuthenticationExtensionsClientOutputs outputs = new ImmutableAuthenticationExtensionsClientOutputs(credentialOutput); + AuthenticatorAssertionResponse response = TestAuthenticationAssertionResponses.createAuthenticatorAssertionResponse() + .build(); + PublicKeyCredential credential = TestPublicKeyCredential.createPublicKeyCredential( + response, outputs) + .build(); + RelyingPartyAuthenticationRequest authRequest = new RelyingPartyAuthenticationRequest( + TestPublicKeyCredentialRequestOptions.create().build(), + credential + ); + WebAuthnAuthenticationRequestToken requestToken = new WebAuthnAuthenticationRequestToken(authRequest); + requestToken.setDetails(details); + generatorByClassName.put(CredentialPropertiesOutput.class, (o) -> credentialOutput); + generatorByClassName.put(ImmutableAuthenticationExtensionsClientOutputs.class, (o) -> outputs); + generatorByClassName.put(AuthenticatorAssertionResponse.class, (r) -> response); + generatorByClassName.put(RelyingPartyAuthenticationRequest.class, (r) -> authRequest); + generatorByClassName.put(PublicKeyCredential.class, (r) -> credential); + generatorByClassName.put(WebAuthnAuthenticationRequestToken.class, (r) -> requestToken); + generatorByClassName.put(AuthenticatorAttachment.class, (r) -> AuthenticatorAttachment.PLATFORM); + // @formatter:on } @ParameterizedTest @@ -277,6 +716,39 @@ static Stream getFilesToDeserialize() throws IOException { return Files.list(previousVersionFolder); } + @Test + void listClassesMissingSerialVersion() throws Exception { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AssignableTypeFilter(Serializable.class)); + List> classes = new ArrayList<>(); + + Set components = provider.findCandidateComponents("org/springframework/security"); + for (BeanDefinition component : components) { + Class clazz = Class.forName(component.getBeanClassName()); + boolean isAbstract = Modifier.isAbstract(clazz.getModifiers()); + if (isAbstract) { + continue; + } + if (clazz.isEnum()) { + continue; + } + if (clazz.getName().contains("Tests")) { + continue; + } + boolean hasSerialVersion = Stream.of(clazz.getDeclaredFields()) + .map(Field::getName) + .anyMatch((n) -> n.equals("serialVersionUID")); + if (!hasSerialVersion) { + classes.add(clazz); + } + } + if (!classes.isEmpty()) { + System.out + .println("Found " + classes.size() + " Serializable classes that don't declare a seriallVersionUID"); + System.out.println(classes.stream().map(Class::getName).collect(Collectors.joining("\r\n"))); + } + } + static Stream> getClassesToSerialize() throws Exception { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); provider.addIncludeFilter(new AssignableTypeFilter(Serializable.class)); @@ -286,9 +758,13 @@ static Stream> getClassesToSerialize() throws Exception { for (BeanDefinition component : components) { Class clazz = Class.forName(component.getBeanClassName()); boolean isAbstract = Modifier.isAbstract(clazz.getModifiers()); + if (isAbstract) { + continue; + } boolean matchesExpectedSerialVersion = ObjectStreamClass.lookup(clazz) .getSerialVersionUID() == securitySerialVersionUid; - if (!isAbstract && matchesExpectedSerialVersion) { + boolean isUnderTest = generatorByClassName.containsKey(clazz); + if (matchesExpectedSerialVersion || isUnderTest) { classes.add(clazz); } } @@ -296,13 +772,23 @@ static Stream> getClassesToSerialize() throws Exception { } private static InstancioApi instancioWithDefaults(Class clazz) { - InstancioApi instancio = Instancio.of(clazz); + InstancioOfClassApi instancio = Instancio.of(clazz); + ResolvableType[] generics = ResolvableType.forClass(clazz).getGenerics(); + for (ResolvableType type : generics) { + instancio.withTypeParameters(type.resolve()); + } if (generatorByClassName.containsKey(clazz)) { instancio.supply(Select.all(clazz), generatorByClassName.get(clazz)); } return instancio; } + private static T applyDetails(T authentication) { + WebAuthenticationDetails details = new WebAuthenticationDetails("remote", "sessionId"); + authentication.setDetails(details); + return authentication; + } + private static String getCurrentVersion() { String version = System.getProperty("springSecurityVersion"); String[] parts = version.split("\\."); diff --git a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java index f8c6c97f6d7..4fdd734dcd9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test; +import org.springframework.security.config.ObjectPostProcessor; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java index 84411b85037..24f3be601f7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; +import org.springframework.security.config.ObjectPostProcessor; import static org.assertj.core.api.Assertions.assertThat; diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java index f869ef5c498..dc2690e3bf4 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java @@ -34,7 +34,7 @@ import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java index 387ca313d47..a25e121073b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java @@ -42,8 +42,8 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AlreadyBuiltException; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java index 09ac66e347c..7730a0606b9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; diff --git a/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java b/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java index f02375507e0..75b5674248f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java @@ -37,7 +37,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.NativeDetector; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.web.context.ServletContextAware; diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 223fc3f8fc2..17d8f8a3a92 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -28,19 +28,28 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.annotation.security.DenyAll; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aop.Advisor; import org.springframework.aop.config.AopConfigUtils; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.Bean; @@ -66,6 +75,7 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; @@ -78,10 +88,12 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestParentApplicationContextExecutionListener; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; @@ -99,11 +111,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link PrePostMethodSecurityConfiguration}. @@ -614,69 +628,77 @@ public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() { assertThat(filtered).containsExactly("DoNotDrop"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.hasRole("USER")).isTrue(); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.hasUserRole()).isTrue(); } - @Test - public void methodWhenParameterizedAnnotationThenFails() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + public void methodWhenParameterizedAnnotationThenFails(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser(authorities = "SCOPE_message:read") - public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.readMessage()).isEqualTo("message"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser(roles = "ADMIN") - public void methodWhenMultiplePlaceholdersHasRoleThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenMultiplePlaceholdersHasRoleThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.readMessage()).isEqualTo("message"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); service.startsWithDave("daveMatthews"); assertThatExceptionOfType(AccessDeniedException.class) .isThrownBy(() -> service.startsWithDave("jenniferHarper")); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodWhenPreFilterMetaAnnotationThenFilters() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenPreFilterMetaAnnotationThenFilters(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul")))) .containsExactly("dave"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodWhenPostFilterMetaAnnotationThenFilters() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenPostFilterMetaAnnotationThenFilters(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul")))) .containsExactly("dave"); @@ -834,7 +856,7 @@ void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUs @WithMockUser void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() { this.spring - .register(MethodSecurityServiceEnabledConfig.class, MetaAnnotationPlaceholderConfig.class, + .register(MethodSecurityServiceEnabledConfig.class, LegacyMetaAnnotationPlaceholderConfig.class, MethodSecurityService.NullPostProcessor.class) .autowire(); MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); @@ -1007,6 +1029,80 @@ public void methodWhenMetaAnnotationPropertiesHasClassProperties() { assertThat(service.getIdPath("uid")).isEqualTo("uid"); } + @Test + @WithMockUser + public void prePostMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.preAuthorizePermitAll(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void securedMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.securedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::secured); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void jsr250MethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.jsr250RolesAllowedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(this.methodSecurityService::jsr250RolesAllowed); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.preAuthorizePermitAll(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize); + verifyNoInteractions(handler); + } + + @Test + @WithMockUser + public void securedMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.securedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verifyNoInteractions(handler); + } + + @Test + @WithMockUser + public void jsr250MethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.jsr250RolesAllowedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verifyNoInteractions(handler); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -1199,6 +1295,8 @@ static class AuthorizationEventPublisherConfig { @Bean AuthorizationEventPublisher authorizationEventPublisher() { + doCallRealMethod().when(this.publisher) + .publishAuthorizationEvent(any(), any(), any(AuthorizationResult.class)); return this.publisher; } @@ -1335,7 +1433,7 @@ Authz authz() { @Configuration @EnableMethodSecurity - static class MetaAnnotationPlaceholderConfig { + static class LegacyMetaAnnotationPlaceholderConfig { @Bean PrePostTemplateDefaults methodSecurityDefaults() { @@ -1349,6 +1447,22 @@ MetaAnnotationService metaAnnotationService() { } + @Configuration + @EnableMethodSecurity + static class MetaAnnotationPlaceholderConfig { + + @Bean + AnnotationTemplateExpressionDefaults methodSecurityDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + + @Bean + MetaAnnotationService metaAnnotationService() { + return new MetaAnnotationService(); + } + + } + static class MetaAnnotationService { @RequireRole(role = "#role") @@ -1628,4 +1742,57 @@ public Class getObjectType() { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java index 5fe335870d7..b82eb985bc9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java @@ -16,18 +16,67 @@ package org.springframework.security.config.annotation.method.configuration; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.annotation.security.DenyAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.security.authorization.method.PrePostTemplateDefaults; +import org.springframework.security.config.Customizer; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.stereotype.Component; import org.springframework.test.context.junit.jupiter.SpringExtension; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + @ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) @SecurityTestExecutionListeners public class PrePostReactiveMethodSecurityConfigurationTests { @@ -201,6 +250,231 @@ void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMet StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete(); } + @Test + @WithMockUser(roles = "ADMIN") + public void preAuthorizeWhenCustomMethodSecurityExpressionHandlerThenUses() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, PermissionEvaluatorConfig.class).autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + PermissionEvaluator permissionEvaluator = this.spring.getContext().getBean(PermissionEvaluator.class); + given(permissionEvaluator.hasPermission(any(), eq("grant"), any())).willReturn(true); + given(permissionEvaluator.hasPermission(any(), eq("deny"), any())).willReturn(false); + StepVerifier.create(service.preAuthorizeHasPermission("grant")).expectNext("ok").verifyComplete(); + StepVerifier.create(service.preAuthorizeHasPermission("deny")) + .expectError(AuthorizationDeniedException.class) + .verify(); + verify(permissionEvaluator, times(2)).hasPermission(any(), any(), any()); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser + public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.hasRole("USER").block()).isTrue(); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser + public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.hasUserRole().block()).isTrue(); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + public void methodWhenParameterizedAnnotationThenFails(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> service.placeholdersOnlyResolvedByMetaAnnotations().block()); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser(authorities = "SCOPE_message:read") + public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.readMessage().block()).isEqualTo("message"); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser(roles = "ADMIN") + public void methodWhenMultiplePlaceholdersHasRoleThenPasses(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.readMessage().block()).isEqualTo("message"); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser + public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + service.startsWithDave("daveMatthews"); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> service.startsWithDave("jenniferHarper").block()); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser + public void methodWhenPreFilterMetaAnnotationThenFilters(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.parametersContainDave(Flux.just("dave", "carla", "vanessa", "paul")).collectList().block()) + .containsExactly("dave"); + } + + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + @WithMockUser + public void methodWhenPostFilterMetaAnnotationThenFilters(Class config) { + this.spring.register(config).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.resultsContainDave(Flux.just("dave", "carla", "vanessa", "paul")).collectList().block()) + .containsExactly("dave"); + } + + @Test + @WithMockUser(authorities = "airplane:read") + public void findByIdWhenAuthorizedResultThenAuthorizes() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + Flight flight = flights.findById("1").block(); + assertThatNoException().isThrownBy(flight::getAltitude); + assertThatNoException().isThrownBy(flight::getSeats); + } + + @Test + @WithMockUser(authorities = "seating:read") + public void findByIdWhenUnauthorizedResultThenDenies() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + Flight flight = flights.findById("1").block(); + assertThatNoException().isThrownBy(flight::getSeats); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block()); + } + + @Test + @WithMockUser(authorities = "seating:read") + public void findAllWhenUnauthorizedResultThenDenies() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + flights.findAll().collectList().block().forEach((flight) -> { + assertThatNoException().isThrownBy(flight::getSeats); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block()); + }); + } + + @Test + public void removeWhenAuthorizedResultThenRemoves() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + flights.remove("1"); + } + + @Test + @WithMockUser(authorities = "airplane:read") + public void findAllWhenPostFilterThenFilters() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + flights.findAll() + .collectList() + .block() + .forEach((flight) -> assertThat(flight.getPassengers().collectList().block()) + .extracting((p) -> p.getName().block()) + .doesNotContain("Kevin Mitnick")); + } + + @Test + @WithMockUser(authorities = "airplane:read") + public void findAllWhenPreFilterThenFilters() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + flights.findAll().collectList().block().forEach((flight) -> { + flight.board(Flux.just("John")).block(); + assertThat(flight.getPassengers().collectList().block()).extracting((p) -> p.getName().block()) + .doesNotContain("John"); + flight.board(Flux.just("John Doe")).block(); + assertThat(flight.getPassengers().collectList().block()).extracting((p) -> p.getName().block()) + .contains("John Doe"); + }); + } + + @Test + @WithMockUser(authorities = "seating:read") + public void findAllWhenNestedPreAuthorizeThenAuthorizes() { + this.spring.register(AuthorizeResultConfig.class).autowire(); + FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class); + flights.findAll().collectList().block().forEach((flight) -> { + List passengers = flight.getPassengers().collectList().block(); + passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> passenger.getName().block())); + }); + } + + // gh-15352 + @Test + void annotationsInChildClassesDoNotAffectSuperclasses() { + this.spring.register(AbstractClassConfig.class).autowire(); + this.spring.getContext().getBean(ClassInheritingAbstractClassWithNoAnnotations.class).method(); + } + + // gh-15592 + @Test + void autowireWhenDefaultsThenCreatesExactlyOneAdvisorPerAnnotation() { + this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire(); + AuthorizationAdvisorProxyFactory proxyFactory = this.spring.getContext() + .getBean(AuthorizationAdvisorProxyFactory.class); + assertThat(proxyFactory).hasSize(5); + assertThat(this.spring.getContext().getBeanNamesForType(AuthorizationAdvisor.class)).hasSize(5) + .containsExactlyInAnyOrder("preFilterAuthorizationMethodInterceptor", + "preAuthorizeAuthorizationMethodInterceptor", "postAuthorizeAuthorizationMethodInterceptor", + "postFilterAuthorizationMethodInterceptor", "authorizeReturnObjectMethodInterceptor"); + } + + // gh-15592 + @Test + void autowireWhenAspectJAutoProxyAndFactoryBeanThenExactlyOneAdvisorPerAnnotation() { + this.spring.register(AspectJAwareAutoProxyAndFactoryBeansConfig.class).autowire(); + AuthorizationAdvisorProxyFactory proxyFactory = this.spring.getContext() + .getBean(AuthorizationAdvisorProxyFactory.class); + assertThat(proxyFactory).hasSize(5); + assertThat(this.spring.getContext().getBeanNamesForType(AuthorizationAdvisor.class)).hasSize(5) + .containsExactlyInAnyOrder("preFilterAuthorizationMethodInterceptor", + "preAuthorizeAuthorizationMethodInterceptor", "postAuthorizeAuthorizationMethodInterceptor", + "postFilterAuthorizationMethodInterceptor", "authorizeReturnObjectMethodInterceptor"); + } + + // gh-15651 + @Test + @WithMockUser(roles = "ADMIN") + public void adviseWhenPrePostEnabledThenEachInterceptorRunsExactlyOnce() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, CustomMethodSecurityExpressionHandlerConfig.class) + .autowire(); + MethodSecurityExpressionHandler expressionHandler = this.spring.getContext() + .getBean(MethodSecurityExpressionHandler.class); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + service.manyAnnotations(Mono.just(new ArrayList<>(Arrays.asList("harold", "jonathan", "tim", "bo")))).block(); + verify(expressionHandler, times(4)).createEvaluationContext(any(Authentication.class), any()); + } + + // gh-15721 + @Test + @WithMockUser(roles = "uid") + public void methodWhenMetaAnnotationPropertiesHasClassProperties() { + this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); + assertThat(service.getIdPath("uid").block()).isEqualTo("uid"); + } + @Configuration @EnableReactiveMethodSecurity static class MethodSecurityServiceEnabledConfig { @@ -212,4 +486,355 @@ ReactiveMethodSecurityService methodSecurityService() { } + @Configuration + @EnableReactiveMethodSecurity + static class CustomMethodSecurityExpressionHandlerConfig { + + private final MethodSecurityExpressionHandler expressionHandler = spy( + new DefaultMethodSecurityExpressionHandler()); + + @Bean + MethodSecurityExpressionHandler methodSecurityExpressionHandler() { + return this.expressionHandler; + } + + } + + @Configuration + static class PermissionEvaluatorConfig { + + @Bean + static PermissionEvaluator permissionEvaluator() { + return mock(PermissionEvaluator.class); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( + PermissionEvaluator permissionEvaluator) { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + handler.setPermissionEvaluator(permissionEvaluator); + return handler; + } + + } + + @Configuration + @EnableReactiveMethodSecurity + static class LegacyMetaAnnotationPlaceholderConfig { + + @Bean + PrePostTemplateDefaults methodSecurityDefaults() { + return new PrePostTemplateDefaults(); + } + + @Bean + MetaAnnotationService metaAnnotationService() { + return new MetaAnnotationService(); + } + + } + + @Configuration + @EnableReactiveMethodSecurity + static class MetaAnnotationPlaceholderConfig { + + @Bean + AnnotationTemplateExpressionDefaults methodSecurityDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + + @Bean + MetaAnnotationService metaAnnotationService() { + return new MetaAnnotationService(); + } + + } + + static class MetaAnnotationService { + + @RequireRole(role = "#role") + Mono hasRole(String role) { + return Mono.just(true); + } + + @RequireRole(role = "'USER'") + Mono hasUserRole() { + return Mono.just(true); + } + + @PreAuthorize("hasRole({role})") + Mono placeholdersOnlyResolvedByMetaAnnotations() { + return Mono.empty(); + } + + @HasClaim(claim = "message:read", roles = { "'ADMIN'" }) + Mono readMessage() { + return Mono.just("message"); + } + + @ResultStartsWith("dave") + Mono startsWithDave(String value) { + return Mono.just(value); + } + + @ParameterContains("dave") + Flux parametersContainDave(Flux list) { + return list; + } + + @ResultContains("dave") + Flux resultsContainDave(Flux list) { + return list; + } + + @RestrictedAccess(entityClass = EntityClass.class) + Mono getIdPath(String id) { + return Mono.just(id); + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @PreAuthorize("hasRole({idPath})") + @interface RestrictedAccess { + + String idPath() default "#id"; + + Class entityClass(); + + String[] recipes() default {}; + + } + + static class EntityClass { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PreAuthorize("hasRole({role})") + @interface RequireRole { + + String role(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})") + @interface HasClaim { + + String claim(); + + String[] roles() default {}; + + } + + @Retention(RetentionPolicy.RUNTIME) + @PostAuthorize("returnObject.startsWith('{value}')") + @interface ResultStartsWith { + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PreFilter("filterObject.contains('{value}')") + @interface ParameterContains { + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PostFilter("filterObject.contains('{value}')") + @interface ResultContains { + + String value(); + + } + + @EnableReactiveMethodSecurity + @Configuration + public static class AuthorizeResultConfig { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static Customizer skipValueTypes() { + return (f) -> f.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.defaultsSkipValueTypes()); + } + + @Bean + FlightRepository flights() { + FlightRepository flights = new FlightRepository(); + Flight one = new Flight("1", 35000d, 35); + one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block(); + flights.save(one).block(); + Flight two = new Flight("2", 32000d, 72); + two.board(Flux.just("Albert Einstein")).block(); + flights.save(two).block(); + return flights; + } + + @Bean + static MethodSecurityExpressionHandler expressionHandler() { + RoleHierarchy hierarchy = RoleHierarchyImpl.withRolePrefix("") + .role("airplane:read") + .implies("seating:read") + .build(); + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setRoleHierarchy(hierarchy); + return expressionHandler; + } + + @Bean + Authz authz() { + return new Authz(); + } + + public static class Authz { + + public Mono isNotKevinMitnick(Passenger passenger) { + return passenger.getName().map((n) -> !"Kevin Mitnick".equals(n)); + } + + } + + } + + @AuthorizeReturnObject + static class FlightRepository { + + private final Map flights = new ConcurrentHashMap<>(); + + Flux findAll() { + return Flux.fromIterable(this.flights.values()); + } + + Mono findById(String id) { + return Mono.just(this.flights.get(id)); + } + + Mono save(Flight flight) { + this.flights.put(flight.getId(), flight); + return Mono.just(flight); + } + + Mono remove(String id) { + this.flights.remove(id); + return Mono.empty(); + } + + } + + @AuthorizeReturnObject + static class Flight { + + private final String id; + + private final Double altitude; + + private final Integer seats; + + private final List passengers = new ArrayList<>(); + + Flight(String id, Double altitude, Integer seats) { + this.id = id; + this.altitude = altitude; + this.seats = seats; + } + + String getId() { + return this.id; + } + + @PreAuthorize("hasAuthority('airplane:read')") + Mono getAltitude() { + return Mono.just(this.altitude); + } + + @PreAuthorize("hasAuthority('seating:read')") + Mono getSeats() { + return Mono.just(this.seats); + } + + @PostAuthorize("hasAuthority('seating:read')") + @PostFilter("@authz.isNotKevinMitnick(filterObject)") + Flux getPassengers() { + return Flux.fromIterable(this.passengers); + } + + @PreAuthorize("hasAuthority('seating:read')") + @PreFilter("filterObject.contains(' ')") + Mono board(Flux passengers) { + return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then(Mono.empty()); + } + + } + + public static class Passenger { + + String name; + + public Passenger(String name) { + this.name = name; + } + + @PreAuthorize("hasAuthority('airplane:read')") + public Mono getName() { + return Mono.just(this.name); + } + + } + + abstract static class AbstractClassWithNoAnnotations { + + Mono method() { + return Mono.just("ok"); + } + + } + + @PreAuthorize("denyAll()") + @Secured("DENIED") + @DenyAll + static class ClassInheritingAbstractClassWithNoAnnotations extends AbstractClassWithNoAnnotations { + + } + + @EnableReactiveMethodSecurity + static class AbstractClassConfig { + + @Bean + ClassInheritingAbstractClassWithNoAnnotations inheriting() { + return new ClassInheritingAbstractClassWithNoAnnotations(); + } + + } + + @Configuration + @EnableReactiveMethodSecurity + static class AspectJAwareAutoProxyAndFactoryBeansConfig { + + @Bean + static BeanDefinitionRegistryPostProcessor beanDefinitionRegistryPostProcessor() { + return AopConfigUtils::registerAspectJAnnotationAutoProxyCreatorIfNecessary; + } + + @Component + static class MyFactoryBean implements FactoryBean { + + @Override + public Object getObject() throws Exception { + return new Object(); + } + + @Override + public Class getObjectType() { + return Object.class; + } + + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java index cf845ebb612..042ed87c7e7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java @@ -23,14 +23,21 @@ import java.util.function.Consumer; import java.util.function.Function; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; @@ -50,6 +57,7 @@ import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; import org.springframework.security.config.Customizer; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; @@ -62,7 +70,9 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Tadaya Tsuyukubo @@ -235,6 +245,46 @@ void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() { verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class)); } + @Test + public void prePostMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.class).autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + Authentication user = TestAuthentication.authenticatedUser(); + StepVerifier + .create(service.preAuthorizeUser().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectNextCount(1) + .verifyComplete(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + StepVerifier + .create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectError() + .verify(); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + Authentication user = TestAuthentication.authenticatedUser(); + StepVerifier + .create(service.preAuthorizeUser().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectNextCount(1) + .verifyComplete(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + StepVerifier + .create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectError() + .verify(); + verifyNoInteractions(handler); + } + private static Consumer authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } @@ -388,4 +438,58 @@ MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() { } + @Configuration + @EnableReactiveMethodSecurity + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java index 000dcb386a0..05e94d8a30e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; import org.aopalliance.intercept.MethodInvocation; import reactor.core.publisher.Mono; @@ -31,7 +32,9 @@ import org.springframework.expression.Expression; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.method.HandleAuthorizationDenied; import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; @@ -45,6 +48,12 @@ @ReactiveMethodSecurityService.Mask("classmask") public interface ReactiveMethodSecurityService { + @PreAuthorize("hasRole('USER')") + Mono preAuthorizeUser(); + + @PreAuthorize("hasRole('ADMIN')") + Mono preAuthorizeAdmin(); + @PreAuthorize("hasRole('ADMIN')") @HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class) Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber); @@ -101,6 +110,15 @@ public interface ReactiveMethodSecurityService { @HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class) Mono checkCustomResult(boolean result); + @PreAuthorize("hasPermission(#kgName, 'read')") + Mono preAuthorizeHasPermission(String kgName); + + @PreAuthorize("hasRole('ADMIN')") + @PostAuthorize("hasRole('ADMIN')") + @PreFilter("true") + @PostFilter("true") + Mono> manyAnnotations(Mono> array); + class StarMaskingHandler implements MethodAuthorizationDeniedHandler { @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java index acf50eb1130..590184f67f6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.method.configuration; +import java.util.List; + import reactor.core.publisher.Mono; import org.springframework.security.authorization.AuthorizationDecision; @@ -23,6 +25,16 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService { + @Override + public Mono preAuthorizeUser() { + return Mono.just("user"); + } + + @Override + public Mono preAuthorizeAdmin() { + return Mono.just("admin"); + } + @Override public Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber) { return Mono.just(cardNumber); @@ -88,4 +100,14 @@ public Mono checkCustomResult(boolean result) { return Mono.just("ok"); } + @Override + public Mono preAuthorizeHasPermission(String kgName) { + return Mono.just("ok"); + } + + @Override + public Mono> manyAnnotations(Mono> array) { + return array; + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/EnableMethodSecurityAotTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/EnableMethodSecurityAotTests.java new file mode 100644 index 00000000000..9e7a047d361 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/EnableMethodSecurityAotTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration.aot; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * AOT Tests for {@code PrePostMethodSecurityConfiguration}. + * + * @author Evgeniy Cheban + * @author Josh Cummings + */ +@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) +public class EnableMethodSecurityAotTests { + + private final ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + private final GenerationContext context = new TestGenerationContext(); + + @Test + void whenProcessAheadOfTimeThenCreatesAuthorizationProxies() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(AppConfig.class); + this.generator.processAheadOfTime(context, this.context); + RuntimeHints hints = this.context.getRuntimeHints(); + assertThat(hints.reflection().getTypeHint(TypeReference.of(cglibClassName(Message.class)))).isNotNull(); + assertThat(hints.reflection().getTypeHint(TypeReference.of(cglibClassName(User.class)))).isNotNull(); + assertThat(hints.proxies() + .jdkProxyHints() + .anyMatch((hint) -> hint.getProxiedInterfaces().contains(TypeReference.of(UserProjection.class)))).isTrue(); + } + + private static String cglibClassName(Class clazz) { + return clazz.getCanonicalName() + "$$SpringCGLIB$$0"; + } + + @Configuration + @EnableMethodSecurity + @EnableJpaRepositories + static class AppConfig { + + @Bean + DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory() { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setGenerateDdl(true); + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setJpaVendorAdapter(vendorAdapter); + factory.setPackagesToScan("org.springframework.security.config.annotation.method.configuration.aot"); + factory.setDataSource(dataSource()); + return factory; + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java new file mode 100644 index 00000000000..2ea294932b1 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration.aot; + +import java.time.Instant; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.method.AuthorizeReturnObject; + +@Entity +public class Message { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String text; + + private String summary; + + private Instant created = Instant.now(); + + @ManyToOne + private User to; + + @AuthorizeReturnObject + public User getTo() { + return this.to; + } + + public void setTo(User to) { + this.to = to; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public Instant getCreated() { + return this.created; + } + + public void setCreated(Instant created) { + this.created = created; + } + + @PreAuthorize("hasAuthority('message:read')") + public String getText() { + return this.text; + } + + public void setText(String text) { + this.text = text; + } + + @PreAuthorize("hasAuthority('message:read')") + public String getSummary() { + return this.summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java new file mode 100644 index 00000000000..9e281e3d6f6 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration.aot; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.stereotype.Repository; + +/** + * A repository for accessing {@link Message}s. + * + * @author Rob Winch + */ +@Repository +@AuthorizeReturnObject +public interface MessageRepository extends CrudRepository { + + @Query("select m from Message m where m.to.id = ?#{ authentication.name }") + Iterable findAll(); + + @Query("from org.springframework.security.config.annotation.method.configuration.aot.User u where u.id = ?#{ authentication.name }") + UserProjection findCurrentUser(); + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java new file mode 100644 index 00000000000..52958356dbe --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.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.config.annotation.method.configuration.aot; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import org.springframework.security.access.prepost.PreAuthorize; + +/** + * A user. + * + * @author Rob Winch + */ +@Entity(name = "users") +public class User { + + @Id + private String id; + + private String firstName; + + private String lastName; + + private String email; + + private String password; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @PreAuthorize("hasAuthority('user:read')") + public String getFirstName() { + return this.firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + @PreAuthorize("hasAuthority('user:read')") + public String getLastName() { + return this.lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java new file mode 100644 index 00000000000..383f76728b1 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration.aot; + +public interface UserProjection { + + String getFirstName(); + + String getLastName(); + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java index 9c2c1f0a146..322459e33ac 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java index 411f92f9cba..8561390515e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java @@ -25,13 +25,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.config.MockServletContext; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.TestMockHttpServletMappings; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; @@ -79,7 +81,11 @@ public O postProcess(O object) { public void setUp() { this.matcherRegistry = new TestRequestMatcherRegistry(); this.context = mock(WebApplicationContext.class); - given(this.context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR); + ObjectProvider> postProcessors = mock(ObjectProvider.class); + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); + ObjectProvider> given = this.context.getBeanProvider(type); + given(given).willReturn(postProcessors); + given(postProcessors.getObject()).willReturn(NO_OP_OBJECT_POST_PROCESSOR); given(this.context.getServletContext()).willReturn(MockServletContext.mvc()); this.matcherRegistry.setApplicationContext(this.context); mockMvcIntrospector(true); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java index 322dd22dea6..ea1c70d0501 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.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. @@ -16,6 +16,11 @@ package org.springframework.security.config.annotation.web.configuration; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,7 +31,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.csrf.CsrfToken; @@ -39,12 +46,15 @@ import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; @@ -97,10 +107,46 @@ public void csrfToken() throws Exception { this.mockMvc.perform(request).andExpect(assertResult(csrfToken)); } + @Test + public void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception { + this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Stranger!")); + Authentication harold = new TestingAuthenticationToken("harold", "password", + AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContextHolder.getContext().setAuthentication(harold); + this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Harold!")); + } + + @Test + public void resolveMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception { + this.mockMvc.perform(get("/hello")).andExpect(content().string("user")); + Authentication harold = new TestingAuthenticationToken("harold", "password", + AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContextHolder.getContext().setAuthentication(harold); + this.mockMvc.perform(get("/hello")).andExpect(content().string("harold")); + } + private ResultMatcher assertResult(Object expected) { return model().attribute("result", expected); } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @CurrentSecurityContext(expression = "authentication.{property}") + @interface CurrentAuthenticationProperty { + + String property(); + + } + @Controller static class TestController { @@ -120,6 +166,24 @@ ModelAndView csrf(CsrfToken token) { return new ModelAndView("view", "result", token); } + @GetMapping("/hi") + @ResponseBody + String ifUser(@IsUser("harold") boolean isHarold) { + if (isHarold) { + return "Hi, Harold!"; + } + else { + return "Hi, Stranger!"; + } + } + + @GetMapping("/hello") + @ResponseBody + String getCurrentAuthenticationProperty( + @CurrentAuthenticationProperty(property = "principal") String principal) { + return principal; + } + } @Configuration @@ -132,6 +196,11 @@ TestController testController() { return new TestController(); } + @Bean + AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index dcdd4e4c332..326e2bda108 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -318,6 +318,14 @@ public void loadConfigWhenMultipleSecurityFilterChainAndIgnoringThenWebInvocatio assertThat(privilegeEvaluator.isAllowed("/ignoring1/child", null)).isTrue(); } + @Test + public void loadConfigWhenTwoSecurityFilterChainsPresentAndSecondWithAnyRequestThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(MultipleAnyRequestSecurityFilterChainConfig.class).autowire()) + .havingRootCause() + .isExactlyInstanceOf(IllegalArgumentException.class); + } + private void assertAnotherUserPermission(WebInvocationPrivilegeEvaluator privilegeEvaluator) { Authentication anotherUser = new TestingAuthenticationToken("anotherUser", "password", "ROLE_ANOTHER"); assertThat(privilegeEvaluator.isAllowed("/user", anotherUser)).isFalse(); @@ -819,4 +827,26 @@ public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { } + @Configuration + @EnableWebSecurity + @EnableWebMvc + @Import(AuthenticationTestConfiguration.class) + static class MultipleAnyRequestSecurityFilterChainConfig { + + @Bean + @Order(0) + SecurityFilterChain api1(HttpSecurity http) throws Exception { + http.authorizeHttpRequests((auth) -> auth.anyRequest().authenticated()); + return http.build(); + } + + @Bean + @Order(1) + SecurityFilterChain api2(HttpSecurity http) throws Exception { + http.securityMatcher("/app/**").authorizeHttpRequests((auth) -> auth.anyRequest().authenticated()); + return http.build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 95c1661416d..41850d67561 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,12 +18,20 @@ import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; @@ -33,16 +41,20 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.authorization.AuthorizationObservationContext; +import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -63,11 +75,15 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -153,7 +169,8 @@ public void configureMvcMatcherAccessAuthorizationManagerWhenNullThenException() @Test public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = this.spring.getContext() + .getBean(ObjectPostProcessorConfig.class).objectPostProcessor; verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class)); verify(objectPostProcessor).postProcess(any(AuthorizationManager.class)); verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class)); @@ -624,6 +641,32 @@ public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Excep this.mvc.perform(requestWithUser).andExpect(status().isOk()); } + @Test + public void getWhenObservationRegistryThenObserves() throws Exception { + this.spring.register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class).autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk()); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthorizationObservationContext); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthorizationObservationContext); + this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden()); + verify(handler).onError(any()); + } + + @Test + public void getWhenExcludeAuthorizationObservationsThenUnobserved() throws Exception { + this.spring + .register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk()); + this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden()); + verifyNoInteractions(handler); + } + @Configuration @EnableWebSecurity static class GrantedAuthorityDefaultHasRoleConfig { @@ -1016,6 +1059,12 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // @formatter:on } + @Bean + UserDetailsService users() { + return new InMemoryUserDetailsManager( + User.withUsername("user").password("{noop}password").roles("USER").build()); + } + } @Configuration @@ -1195,6 +1244,8 @@ static class AuthorizationEventPublisherConfig { @Bean AuthorizationEventPublisher authorizationEventPublisher() { + doCallRealMethod().when(this.publisher) + .publishAuthorizationEvent(any(), any(), any(AuthorizationResult.class)); return this.publisher; } @@ -1213,4 +1264,57 @@ void rootPost() { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index 3697ef1b1cb..8ff4f513611 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java index 3b44f86ed83..6d683c4899a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.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. @@ -22,12 +22,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.MessageSourceAccessor; import org.springframework.mock.web.MockHttpSession; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -38,6 +40,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.DefaultCsrfToken; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; @@ -46,6 +49,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -55,6 +59,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -72,6 +77,8 @@ public class DefaultLoginPageConfigurerTests { @Autowired MockMvc mvc; + MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + @Test public void getWhenFormLoginEnabledThenRedirectsToLoginPage() throws Exception { this.spring.register(DefaultLoginPageConfig.class).autowire(); @@ -87,34 +94,40 @@ public void loginPageThenDefaultLoginPageIsRendered() throws Exception { this.mvc.perform(get("/login").sessionAttr(csrfAttributeName, csrfToken)) .andExpect((result) -> { CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); - assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
    \n" - + "
    \n" - + " \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "\n" - + " \n" - + "
    \n" - + "
    \n" - + ""); + assertThat(result.getResponse().getContentAsString()).isEqualTo(""" + + + + + + + + Please sign in + + + +
    + + + + +
    + + """.formatted(token.getToken())); }); // @formatter:on } @@ -135,34 +148,43 @@ public void loginPageWhenErrorThenDefaultLoginPageWithError() throws Exception { this.mvc.perform(get("/login?error").session((MockHttpSession) mvcResult.getRequest().getSession()) .sessionAttr(csrfAttributeName, csrfToken)) .andExpect((result) -> { + String badCredentialsLocalizedMessage = this.messages + .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"); CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); - assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
    \n" - + "
    \n" - + " \n" - + "
    Bad credentials

    \n" - + " \n" - + " \n" - + "

    \n" + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "\n" - + " \n" - + "
    \n" - + "
    \n" - + ""); + assertThat(result.getResponse().getContentAsString()).isEqualTo(""" + + + + + + + + Please sign in + + + +
    + + + + +
    + + """.formatted(badCredentialsLocalizedMessage, token.getToken())); }); // @formatter:on } @@ -188,36 +210,50 @@ public void loginPageWhenLoggedOutThenDefaultLoginPageWithLogoutMessage() throws this.mvc.perform(get("/login?logout").sessionAttr(csrfAttributeName, csrfToken)) .andExpect((result) -> { CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); - assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
    \n" - + "
    \n" - + " \n" - + "
    You have been signed out

    \n" - + " \n" - + " \n" - + "

    \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "\n" - + " \n" - + "
    \n" - + "
    \n" - + ""); + assertThat(result.getResponse().getContentAsString()).isEqualTo(""" + + + + + + + + Please sign in + + + +
    + + + + +
    + + """.formatted(token.getToken())); }); - // @formatter:on + } + + @Test + public void cssWhenFormLoginConfiguredThenServesCss() throws Exception { + this.spring.register(DefaultLoginPageConfig.class).autowire(); + this.mvc.perform(get("/default-ui.css")) + .andExpect(status().isOk()) + .andExpect(header().string("content-type", "text/css;charset=UTF-8")) + .andExpect(content().string(containsString("body {"))); } @Test @@ -241,35 +277,40 @@ public void loginPageWhenRememberConfigureThenDefaultLoginPageWithRememberMeChec this.mvc.perform(get("/login").sessionAttr(csrfAttributeName, csrfToken)) .andExpect((result) -> { CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); - assertThat(result.getResponse().getContentAsString()).isEqualTo("\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
    \n" - + "
    \n" - + " \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "

    Remember me on this computer.

    \n" - + "\n" - + " \n" - + "
    \n" - + "
    \n" - + ""); + assertThat(result.getResponse().getContentAsString()).isEqualTo(""" + + + + + + + + Please sign in + + + +
    + + + + +
    + + """.formatted(token.getToken())); }); // @formatter:on } @@ -315,6 +356,22 @@ public void configureWhenAuthenticationEntryPointThenNoDefaultLoginPageGeneratin .count()).isZero(); } + @Test + public void configureWhenAuthenticationEntryPointThenDoesNotServeCss() throws Exception { + this.spring.register(DefaultLoginWithCustomAuthenticationEntryPointConfig.class).autowire(); + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + assertThat(filterChain.getFilterChains() + .get(0) + .getFilters() + .stream() + .filter((filter) -> filter.getClass().isAssignableFrom(DefaultResourcesFilter.class)) + .count()).isZero(); + //@formatter:off + this.mvc.perform(get("/default-ui.css")) + .andExpect(status().is3xxRedirection()); + //@formatter:on + } + @Test public void formLoginWhenLogoutEnabledThenCreatesDefaultLogoutPage() throws Exception { this.spring.register(DefaultLogoutPageConfig.class).autowire(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java index 00b678c22e0..d89526127e2 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java @@ -27,7 +27,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java index 4d163bbbd4a..ebb6b48bae8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java @@ -39,7 +39,7 @@ import org.springframework.security.access.vote.AffirmativeBased; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.RememberMeAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 48313a867b3..49b8ed2a1af 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -23,7 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java index 31468c5711f..2a75aa5cc91 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java @@ -23,7 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java index 25cfac15e6f..421289ddf4f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java @@ -16,20 +16,30 @@ package org.springframework.security.config.annotation.web.configurers; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationObservationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.AuthenticationException; @@ -50,8 +60,11 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.springframework.security.config.Customizer.withDefaults; @@ -161,6 +174,43 @@ public void httpBasicWhenUsingCustomSecurityContextRepositoryThenUses() throws E .saveContext(any(SecurityContext.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); } + @Test + public void httpBasicWhenObservationRegistryThenObserves() throws Exception { + this.spring.register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.class).autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthenticationObservationContext); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthenticationObservationContext); + this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized()); + verify(handler).onError(context.capture()); + assertThat(context.getValue()).isInstanceOf(AuthenticationObservationContext.class); + } + + @Test + public void httpBasicWhenExcludeAuthenticationObservationsThenUnobserved() throws Exception { + this.spring + .register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext); + context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext); + this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized()); + verify(handler, never()).onError(any()); + } + @Configuration @EnableWebSecurity static class ObjectPostProcessorConfig { @@ -384,4 +434,57 @@ String home(@AuthenticationPrincipal UserDetails user) { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthentications(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java index 57ffcff48a8..327c755a6aa 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java @@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java index aff4d8a3758..ea62cd7046b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java @@ -29,7 +29,7 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -110,8 +110,8 @@ public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherInLambdaThe @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - ObjectPostProcessor objectPostProcessor = this.spring.getContext() - .getBean(ObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = this.spring.getContext() + .getBean(ObjectPostProcessorConfig.class).objectPostProcessor; verify(objectPostProcessor).postProcess(any(LogoutFilter.class)); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.java index 48758ad2687..e6216a20751 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.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. @@ -17,6 +17,7 @@ package org.springframework.security.config.annotation.web.configurers; import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -52,6 +53,7 @@ public class NamespaceHttpFirewallTests { MockMvc mvc; @Test + @Disabled("MockMvc uses UriComponentsBuilder::fromUriString which was changed in https://github.com/spring-projects/spring-framework/issues/32513") public void requestWhenPathContainsDoubleDotsThenBehaviorMatchesNamespace() throws Exception { this.rule.register(HttpFirewallConfig.class).autowire(); this.mvc.perform(get("/public/../private/")).andExpect(status().isBadRequest()); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index c6bab8cf229..e3cb83f76fd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -33,7 +33,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -60,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.startsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -74,6 +75,7 @@ import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -102,7 +104,7 @@ public void postWhenNoUserDetailsServiceThenException() { @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnRememberMeAuthenticationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - verify(this.spring.getContext().getBean(ObjectPostProcessor.class)) + verify(this.spring.getContext().getBean(ObjectPostProcessorConfig.class).objectPostProcessor) .postProcess(any(RememberMeAuthenticationFilter.class)); } @@ -334,6 +336,27 @@ public void getWhenCustomSecurityContextRepositoryThenUses() throws Exception { verify(repository).saveContext(any(), any(), any()); } + @Test + public void rememberMeExpiresSessionWhenSessionManagementMaximumSessionsExceeds() throws Exception { + this.spring.register(RememberMeMaximumSessionsConfig.class).autowire(); + + MockHttpServletRequestBuilder loginRequest = post("/login").with(csrf()) + .param("username", "user") + .param("password", "password") + .param("remember-me", "true"); + MvcResult mvcResult = this.mvc.perform(loginRequest).andReturn(); + Cookie rememberMeCookie = mvcResult.getResponse().getCookie("remember-me"); + HttpSession session = mvcResult.getRequest().getSession(); + + MockHttpServletRequestBuilder exceedsMaximumSessionsRequest = get("/abc").cookie(rememberMeCookie); + this.mvc.perform(exceedsMaximumSessionsRequest); + + MockHttpServletRequestBuilder sessionExpiredRequest = get("/abc").cookie(rememberMeCookie) + .session((MockHttpSession) session); + this.mvc.perform(sessionExpiredRequest) + .andExpect(content().string(startsWith("This session has been expired"))); + } + @Configuration @EnableWebSecurity static class NullUserDetailsConfig { @@ -617,6 +640,35 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } + @Configuration + @EnableWebSecurity + static class RememberMeMaximumSessionsConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authorizeRequests) -> + authorizeRequests + .anyRequest().hasRole("USER") + ) + .sessionManagement((sessionManagement) -> + sessionManagement + .maximumSessions(1) + ) + .formLogin(withDefaults()) + .rememberMe(withDefaults()); + return http.build(); + // @formatter:on + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); + } + + } + @Configuration @EnableWebSecurity static class SecurityContextRepositoryConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index 1b14a87b335..f22e55043d9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -29,7 +29,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java index fee452f1282..5de55764f45 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java @@ -28,8 +28,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.TestDeferredSecurityContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.TestHttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java index 8a5993e25eb..9c1a47cf90c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java @@ -34,7 +34,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index 35e082e69fc..fbe52459a45 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -41,8 +41,8 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.TestDeferredSecurityContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java new file mode 100644 index 00000000000..a90c43f3122 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Daniel Garnier-Moiroux + */ +@ExtendWith(SpringTestContextExtension.class) +public class WebAuthnConfigurerTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + MockMvc mvc; + + @Test + public void webauthnWhenConfiguredConfiguredThenServesJavascript() throws Exception { + this.spring.register(DefaultWebauthnConfiguration.class).autowire(); + this.mvc.perform(get("/login/webauthn.js")) + .andExpect(status().isOk()) + .andExpect(header().string("content-type", "text/javascript;charset=UTF-8")) + .andExpect(content().string(containsString("async function authenticate("))); + } + + @Test + public void webauthnWhenConfiguredConfiguredThenServesCss() throws Exception { + this.spring.register(DefaultWebauthnConfiguration.class).autowire(); + this.mvc.perform(get("/default-ui.css")) + .andExpect(status().isOk()) + .andExpect(header().string("content-type", "text/css;charset=UTF-8")) + .andExpect(content().string(containsString("body {"))); + } + + @Test + public void webauthnWhenNoFormLoginAndDefaultRegistrationPageConfiguredThenServesJavascript() throws Exception { + this.spring.register(NoFormLoginAndDefaultRegistrationPageConfiguration.class).autowire(); + this.mvc.perform(get("/login/webauthn.js")) + .andExpect(status().isOk()) + .andExpect(header().string("content-type", "text/javascript;charset=UTF-8")) + .andExpect(content().string(containsString("async function authenticate("))); + } + + @Test + public void webauthnWhenNoFormLoginAndDefaultRegistrationPageConfiguredThenServesCss() throws Exception { + this.spring.register(NoFormLoginAndDefaultRegistrationPageConfiguration.class).autowire(); + this.mvc.perform(get("/default-ui.css")) + .andExpect(status().isOk()) + .andExpect(header().string("content-type", "text/css;charset=UTF-8")) + .andExpect(content().string(containsString("body {"))); + } + + @Test + public void webauthnWhenFormLoginAndDefaultRegistrationPageConfiguredThenNoDuplicateFilters() { + this.spring.register(DefaultWebauthnConfiguration.class).autowire(); + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + + List defaultResourcesFilters = filterChain.getFilterChains() + .get(0) + .getFilters() + .stream() + .filter(DefaultResourcesFilter.class::isInstance) + .map(DefaultResourcesFilter.class::cast) + .toList(); + + assertThat(defaultResourcesFilters).map(DefaultResourcesFilter::toString) + .filteredOn((filterDescription) -> filterDescription.contains("login/webauthn.js")) + .hasSize(1); + assertThat(defaultResourcesFilters).map(DefaultResourcesFilter::toString) + .filteredOn((filterDescription) -> filterDescription.contains("default-ui.css")) + .hasSize(1); + } + + @Test + public void webauthnWhenConfiguredAndFormLoginThenDoesServesJavascript() throws Exception { + this.spring.register(FormLoginAndNoDefaultRegistrationPageConfiguration.class).autowire(); + this.mvc.perform(get("/login/webauthn.js")) + .andExpect(status().isOk()) + .andExpect(header().string("content-type", "text/javascript;charset=UTF-8")) + .andExpect(content().string(containsString("async function authenticate("))); + } + + @Test + public void webauthnWhenConfiguredAndNoDefaultRegistrationPageThenDoesNotServeJavascript() throws Exception { + this.spring.register(NoDefaultRegistrationPageConfiguration.class).autowire(); + this.mvc.perform(get("/login/webauthn.js")).andExpect(status().isNotFound()); + } + + @Configuration + @EnableWebSecurity + static class DefaultWebauthnConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.formLogin(Customizer.withDefaults()).webAuthn(Customizer.withDefaults()).build(); + } + + } + + @Configuration + @EnableWebSecurity + static class NoFormLoginAndDefaultRegistrationPageConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.webAuthn(Customizer.withDefaults()).build(); + } + + } + + @Configuration + @EnableWebSecurity + static class FormLoginAndNoDefaultRegistrationPageConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.formLogin(Customizer.withDefaults()) + .webAuthn((webauthn) -> webauthn.disableDefaultRegistrationPage(true)) + .build(); + } + + } + + @Configuration + @EnableWebSecurity + static class NoDefaultRegistrationPageConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.formLogin((login) -> login.loginPage("/custom-login-page")) + .webAuthn((webauthn) -> webauthn.disableDefaultRegistrationPage(true)) + .build(); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index f177dd4ffb6..206c0b7ecee 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 83dacaa265b..42d6ab71032 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -286,6 +286,22 @@ public void configureWhenCustomAuthorizationRedirectStrategySetThenAuthorization verify(authorizationRedirectStrategy).sendRedirect(any(), any(), anyString()); } + @Test + public void configureWhenCustomAuthorizationRequestResolverBeanPresentThenAuthorizationRequestResolverUsed() + throws Exception { + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = authorizationRequestResolver; + authorizationRequestResolver = mock(OAuth2AuthorizationRequestResolver.class); + given(authorizationRequestResolver.resolve(any())) + .willAnswer((invocation) -> defaultAuthorizationRequestResolver.resolve(invocation.getArgument(0))); + this.spring.register(OAuth2ClientInLambdaConfig.class, AuthorizationRequestResolverConfig.class).autowire(); + // @formatter:off + this.mockMvc.perform(get("/oauth2/authorization/registration-1")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + // @formatter:on + verify(authorizationRequestResolver).resolve(any()); + } + @Test public void configureWhenOAuth2LoginBeansConfiguredThenNotShared() throws Exception { this.spring.register(OAuth2ClientConfigWithOAuth2Login.class).autowire(); @@ -406,6 +422,16 @@ OAuth2AuthorizedClientRepository authorizedClientRepository() { } + @Configuration + static class AuthorizationRequestResolverConfig { + + @Bean + OAuth2AuthorizationRequestResolver authorizationRequestResolver() { + return authorizationRequestResolver; + } + + } + @Configuration @EnableWebSecurity @EnableWebMvc diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java index 71595ac7aa7..9dc542a406b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java @@ -19,44 +19,55 @@ import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens; +import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; +import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import static org.assertj.core.api.Assertions.assertThat; public class OidcBackChannelLogoutHandlerTests { + private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); + + private final OidcBackChannelLogoutAuthentication token = new OidcBackChannelLogoutAuthentication( + TestOidcLogoutTokens.withSubject("issuer", "subject").build(), + TestClientRegistrations.clientRegistration().build()); + // gh-14553 @Test public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); + OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout"); + logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout"); request.setServerName("host.docker.internal"); request.setServerPort(8090); - String endpoint = logoutHandler.computeLogoutEndpoint(request); - assertThat(endpoint).isEqualTo("http://localhost:8090/logout"); + String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token); + assertThat(endpoint).startsWith("http://localhost:8090/logout"); } @Test public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); + OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry); logoutHandler.setLogoutUri("{baseUrl}/logout"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout"); request.setServerName("host.docker.internal"); request.setServerPort(8090); - String endpoint = logoutHandler.computeLogoutEndpoint(request); - assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout"); + String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token); + assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout"); } // gh-14609 @Test public void computeLogoutEndpointWhenLogoutUriThenUses() { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); + OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry); logoutHandler.setLogoutUri("http://localhost:8090/logout"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout"); request.setScheme("https"); request.setServerName("server-one.com"); request.setServerPort(80); - String endpoint = logoutHandler.computeLogoutEndpoint(request); - assertThat(endpoint).isEqualTo("http://localhost:8090/logout"); + String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token); + assertThat(endpoint).startsWith("http://localhost:8090/logout"); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java index 5f3f9816f3d..653b9f6f67f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java @@ -24,8 +24,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; -import com.gargoylesoftware.htmlunit.util.UrlUtils; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; @@ -41,6 +41,7 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.htmlunit.util.UrlUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -91,6 +92,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; @@ -219,6 +221,40 @@ void logoutWhenRemoteLogoutUriThenUses() throws Exception { this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk()); } + @Test + void logoutWhenSelfRemoteLogoutUriThenUses() throws Exception { + this.spring.register(WebServerConfig.class, OidcProviderConfig.class, SelfLogoutUriConfig.class).autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + MockHttpSession session = login(); + String logoutToken = this.mvc.perform(get("/token/logout").session(session)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + this.mvc + .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .param("logout_token", logoutToken)) + .andExpect(status().isOk()); + this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized()); + } + + @Test + void logoutWhenDifferentCookieNameThenUses() throws Exception { + this.spring.register(OidcProviderConfig.class, CookieConfig.class).autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + MockHttpSession session = login(); + String logoutToken = this.mvc.perform(get("/token/logout").session(session)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + this.mvc + .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .param("logout_token", logoutToken)) + .andExpect(status().isOk()); + this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized()); + } + @Test void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception { this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire(); @@ -372,6 +408,83 @@ SecurityFilterChain filters(HttpSecurity http) throws Exception { } + @Configuration + @EnableWebSecurity + @Import(RegistrationConfig.class) + static class SelfLogoutUriConfig { + + @Bean + @Order(1) + SecurityFilterChain filters(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc + .backChannel(Customizer.withDefaults()) + ); + // @formatter:on + + return http.build(); + } + + } + + @Configuration + @EnableWebSecurity + @Import(RegistrationConfig.class) + static class CookieConfig { + + private final MockWebServer server = new MockWebServer(); + + @Bean + @Order(1) + SecurityFilterChain filters(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc + .backChannel(Customizer.withDefaults()) + ); + // @formatter:on + + return http.build(); + } + + @Bean + OidcSessionRegistry sessionRegistry() { + return new InMemoryOidcSessionRegistry(); + } + + @Bean + OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) { + OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry); + logoutHandler.setSessionCookieName("SESSION"); + return logoutHandler; + } + + @Bean + MockWebServer web(ObjectProvider mvc) { + MockMvcDispatcher dispatcher = new MockMvcDispatcher(mvc); + dispatcher.setAssertion((rr) -> { + String cookie = rr.getHeaders().get("Cookie"); + if (cookie == null) { + return; + } + assertThat(cookie).contains("SESSION").doesNotContain("JSESSIONID"); + }); + this.server.setDispatcher(dispatcher); + return this.server; + } + + @PreDestroy + void shutdown() throws IOException { + this.server.shutdown(); + } + + } + @Configuration @EnableWebSecurity @Import(RegistrationConfig.class) @@ -385,7 +498,7 @@ SecurityFilterChain filters(HttpSecurity http) throws Exception { // @formatter:off http .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry)) + .oauth2Login(Customizer.withDefaults()) .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); // @formatter:on @@ -634,12 +747,15 @@ private static class MockMvcDispatcher extends Dispatcher { private MockMvc mvc; + private Consumer assertion = (rr) -> { }; + MockMvcDispatcher(ObjectProvider mvc) { this.mvcProvider = mvc; } @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + this.assertion.accept(request); this.mvc = this.mvcProvider.getObject(); String method = request.getMethod(); String path = request.getPath(); @@ -676,6 +792,10 @@ void registerSession(MockHttpSession session) { this.session.put(session.getId(), session); } + void setAssertion(Consumer assertion) { + this.assertion = assertion; + } + private MockHttpSession session(RecordedRequest request) { String cookieHeaderValue = request.getHeader("Cookie"); if (cookieHeaderValue == null) { @@ -688,6 +808,10 @@ private MockHttpSession session(RecordedRequest request) { return this.session.computeIfAbsent(parts[1], (k) -> new MockHttpSession(new MockServletContext(), parts[1])); } + if ("SESSION".equals(parts[0])) { + return this.session.computeIfAbsent(parts[1], + (k) -> new MockHttpSession(new MockServletContext(), parts[1])); + } } return new MockHttpSession(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index cb2ba0e137d..c247a6d7fed 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -82,7 +82,7 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java new file mode 100644 index 00000000000..72474ee8258 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java @@ -0,0 +1,340 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.ott; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.ott.OneTimeToken; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; +import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringTestContextExtension.class) +public class OneTimeTokenLoginConfigurerTests { + + public SpringTestContext spring = new SpringTestContext(this); + + @Autowired(required = false) + MockMvc mvc; + + @Test + void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception { + this.spring.register(OneTimeTokenDefaultConfig.class).autowire(); + this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/login/ott")); + + String token = getLastToken().getTokenValue(); + + this.mvc.perform(post("/login/ott").param("token", token).with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/"), authenticated()); + } + + @Test + void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() throws Exception { + this.spring.register(OneTimeTokenDifferentUrlsConfig.class).autowire(); + this.mvc.perform(post("/generateurl").param("username", "user").with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/redirected")); + + String token = getLastToken().getTokenValue(); + + this.mvc.perform(post("/loginprocessingurl").param("token", token).with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/authenticated"), authenticated()); + } + + @Test + void oneTimeTokenWhenCorrectTokenUsedTwiceThenSecondTimeFails() throws Exception { + this.spring.register(OneTimeTokenDefaultConfig.class).autowire(); + this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/login/ott")); + + String token = getLastToken().getTokenValue(); + + this.mvc.perform(post("/login/ott").param("token", token).with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/"), authenticated()); + + this.mvc.perform(post("/login/ott").param("token", token).with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/login?error"), unauthenticated()); + } + + @Test + void oneTimeTokenWhenWrongTokenThenAuthenticationFail() throws Exception { + this.spring.register(OneTimeTokenDefaultConfig.class).autowire(); + this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/login/ott")); + + String token = "wrong"; + + this.mvc.perform(post("/login/ott").param("token", token).with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/login?error"), unauthenticated()); + } + + @Test + void oneTimeTokenWhenConfiguredThenServesCss() throws Exception { + this.spring.register(OneTimeTokenDefaultConfig.class).autowire(); + this.mvc.perform(get("/default-ui.css")) + .andExpect(status().isOk()) + .andExpect(content().string(Matchers.containsString("body {"))); + } + + @Test + void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() throws Exception { + this.spring.register(OneTimeTokenFormLoginConfig.class).autowire(); + CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "BaseSpringSpec_CSRFTOKEN"); + String csrfAttributeName = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN"); + //@formatter:off + this.mvc.perform(get("/login").sessionAttr(csrfAttributeName, csrfToken)) + .andExpect((result) -> { + CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName()); + assertThat(result.getResponse().getContentAsString()).isEqualTo( + """ + + + + + + + + Please sign in + + + +
    + + + + +
    + + """.formatted(token.getToken(), token.getToken())); + }); + //@formatter:on + } + + @Test + void oneTimeTokenWhenNoTokenGenerationSuccessHandlerThenException() { + assertThatException() + .isThrownBy(() -> this.spring.register(OneTimeTokenNoGeneratedOttHandlerConfig.class).autowire()) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage(""" + A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin(). + Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL. + """); + } + + private OneTimeToken getLastToken() { + OneTimeToken lastToken = this.spring.getContext() + .getBean(TestOneTimeTokenGenerationSuccessHandler.class).lastToken; + return lastToken; + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @Import(UserDetailsServiceConfig.class) + static class OneTimeTokenDefaultConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, + OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated() + ) + .oneTimeTokenLogin((ott) -> ott + .tokenGenerationSuccessHandler(ottSuccessHandler) + ); + // @formatter:on + return http.build(); + } + + @Bean + TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestOneTimeTokenGenerationSuccessHandler(); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @Import(UserDetailsServiceConfig.class) + static class OneTimeTokenDifferentUrlsConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, + OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated() + ) + .oneTimeTokenLogin((ott) -> ott + .tokenGeneratingUrl("/generateurl") + .tokenGenerationSuccessHandler(ottSuccessHandler) + .loginProcessingUrl("/loginprocessingurl") + .authenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/authenticated")) + ); + // @formatter:on + return http.build(); + } + + @Bean + TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestOneTimeTokenGenerationSuccessHandler("/redirected"); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @Import(UserDetailsServiceConfig.class) + static class OneTimeTokenFormLoginConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, + OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated() + ) + .formLogin(Customizer.withDefaults()) + .oneTimeTokenLogin((ott) -> ott + .tokenGenerationSuccessHandler(ottSuccessHandler) + ); + // @formatter:on + return http.build(); + } + + @Bean + TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestOneTimeTokenGenerationSuccessHandler(); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @Import(UserDetailsServiceConfig.class) + static class OneTimeTokenNoGeneratedOttHandlerConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated() + ) + .oneTimeTokenLogin(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + } + + static class TestOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { + + private OneTimeToken lastToken; + + private final OneTimeTokenGenerationSuccessHandler delegate; + + TestOneTimeTokenGenerationSuccessHandler() { + this.delegate = new RedirectOneTimeTokenGenerationSuccessHandler("/login/ott"); + } + + TestOneTimeTokenGenerationSuccessHandler(String redirectUrl) { + this.delegate = new RedirectOneTimeTokenGenerationSuccessHandler(redirectUrl); + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) + throws IOException, ServletException { + this.lastToken = oneTimeToken; + this.delegate.handle(request, response, oneTimeToken); + } + + } + + @Configuration(proxyBeanMethods = false) + static class UserDetailsServiceConfig { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin()); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 461f030e9f2..6d874a583d6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -25,13 +25,14 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.instancio.internal.util.ReflectionUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.opensaml.core.Version; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.Marshaller; import org.opensaml.saml.saml2.core.Assertion; @@ -69,6 +70,7 @@ import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; @@ -83,6 +85,7 @@ import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; @@ -91,6 +94,7 @@ import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -101,6 +105,7 @@ import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.startsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeastOnce; @@ -113,6 +118,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -132,6 +138,8 @@ public class Saml2LoginConfigurerTests { .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) .build(); + private static final boolean USE_OPENSAML_5 = Version.getVersion().startsWith("5"); + private static String SIGNED_RESPONSE; private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class); @@ -172,7 +180,11 @@ static void createResponse() throws Exception { registration.getSigningX509Credentials().iterator().next(), relyingPartyEntityId); Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(signed); Element element = marshaller.marshall(signed); - String serialized = SerializeSupport.nodeToString(element); + Class clazz = ReflectionUtils.loadClass("net.shibboleth.utilities.java.support.xml.SerializeSupport"); + if (clazz == null) { + clazz = ReflectionUtils.loadClass("net.shibboleth.shared.xml.SerializeSupport"); + } + String serialized = ReflectionTestUtils.invokeMethod(clazz, "nodeToString", element); SIGNED_RESPONSE = Saml2Utils.samlEncode(serialized.getBytes(StandardCharsets.UTF_8)); } @@ -306,7 +318,7 @@ public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses() Saml2AuthenticationException exception = captor.getValue(); assertThat(exception.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE); assertThat(exception.getSaml2Error().getDescription()).isEqualTo("Unable to inflate string"); - assertThat(exception.getCause()).isInstanceOf(IOException.class); + assertThat(exception).hasRootCauseInstanceOf(IOException.class); } @Test @@ -343,6 +355,19 @@ public void authenticationRequestWhenCustomAuthenticationRequestUriRepositoryThe any(HttpServletRequest.class), any(HttpServletResponse.class)); } + @Test + public void authenticationRequestWhenCustomAuthenticationRequestPathRepositoryThenUses() throws Exception { + this.spring.register(CustomAuthenticationRequestUriQuery.class).autowire(); + MockHttpServletRequestBuilder request = get("/custom/auth/sso"); + this.mvc.perform(request) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/custom/auth/sso?entityId=registration-id")); + request.queryParam("entityId", registration.getRegistrationId()); + MvcResult result = this.mvc.perform(request).andExpect(status().isFound()).andReturn(); + String redirectedUrl = result.getResponse().getRedirectedUrl(); + assertThat(redirectedUrl).startsWith(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + } + @Test public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenAutowires() throws Exception { @@ -390,7 +415,7 @@ public void getFaviconWhenDefaultConfigurationThenDoesNotSaveAuthnRequest() thro .andExpect(redirectedUrl("http://localhost/login")); this.mvc.perform(get("/").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/saml2/authenticate/registration-id")); + .andExpect(header().string("Location", startsWith("http://localhost/saml2/authenticate"))); } @Test @@ -526,6 +551,12 @@ Saml2AuthenticationRequestResolver authenticationRequestResolver( RelyingPartyRegistrationRepository registrations) { RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( registrations); + if (USE_OPENSAML_5) { + OpenSaml5AuthenticationRequestResolver delegate = new OpenSaml5AuthenticationRequestResolver( + registrationResolver); + delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); + return delegate; + } OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver( registrationResolver); delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); @@ -559,6 +590,12 @@ Saml2AuthenticationRequestResolver authenticationRequestResolver( RelyingPartyRegistrationRepository registrations) { RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver( registrations); + if (USE_OPENSAML_5) { + OpenSaml5AuthenticationRequestResolver delegate = new OpenSaml5AuthenticationRequestResolver( + registrationResolver); + delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); + return delegate; + } OpenSaml4AuthenticationRequestResolver delegate = new OpenSaml4AuthenticationRequestResolver( registrationResolver); delegate.setAuthnRequestCustomizer((parameters) -> parameters.getAuthnRequest().setForceAuthn(true)); @@ -669,6 +706,23 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { } + @Configuration + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class CustomAuthenticationRequestUriQuery { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authz) -> authz.anyRequest().authenticated()) + .saml2Login((saml2) -> saml2.authenticationRequestUriQuery("/custom/auth/sso?entityId={registrationId}")); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity @Import(Saml2LoginConfigBeans.class) @@ -720,7 +774,8 @@ Saml2AuthenticationTokenConverter authenticationTokenConverter() { @Import(Saml2LoginConfigBeans.class) static class CustomAuthenticationProviderConfig { - private final OpenSaml4AuthenticationProvider provider = spy(new OpenSaml4AuthenticationProvider()); + private final AuthenticationProvider provider = spy( + USE_OPENSAML_5 ? new OpenSaml5AuthenticationProvider() : new OpenSaml4AuthenticationProvider()); @Bean SecurityFilterChain web(HttpSecurity http) throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java index 8926f0092b7..e13bddf7073 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -42,7 +42,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java index a7ba5b53de2..e75eff1f572 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java @@ -30,7 +30,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; +import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; @@ -159,7 +159,7 @@ SecurityFilterChain filters(HttpSecurity http) throws Exception { // should ignore @Bean Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) { - return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver()); + return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index 890af2bfa07..a2c0a7de2ff 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web.reactive; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -28,6 +32,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.Order; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -404,11 +409,28 @@ public String username(UserDetails user) { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + @interface Property { + + @AliasFor(attribute = "expression", annotation = AuthenticationPrincipal.class) + String value() default "id"; + + } + + interface UsernameResolver { + + String username(@Property("@principalBean.username(#this)") String username); + + } + @RestController - static class AuthenticationPrincipalResolver { + static class AuthenticationPrincipalResolver implements UsernameResolver { + @Override @GetMapping("/spel") - String username(@AuthenticationPrincipal(expression = "@principalBean.username(#this)") String username) { + public String username(String username) { return username; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java index 972a6eb539c..4129702b344 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,26 +16,43 @@ package org.springframework.security.config.annotation.web.reactive; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.net.URI; +import java.util.Iterator; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.password.CompromisedPasswordDecision; import org.springframework.security.authentication.password.CompromisedPasswordException; import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker; import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.server.DefaultServerRedirectStrategy; @@ -43,12 +60,22 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockAuthentication; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; /** * Tests for {@link ServerHttpSecurityConfiguration}. @@ -67,7 +94,10 @@ void setup(ApplicationContext context) { if (!context.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) { return; } - this.webClient = WebTestClient.bindToApplicationContext(context).configureClient().build(); + this.webClient = WebTestClient.bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .build(); } @Test @@ -146,6 +176,97 @@ void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToR // @formatter:on } + @Test + public void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception { + this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + this.webClient.mutateWith(mockAuthentication(user)) + .get() + .uri("/hi") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("Hi, Stranger!"); + Authentication harold = new TestingAuthenticationToken("harold", "password", "ROLE_USER"); + this.webClient.mutateWith(mockAuthentication(harold)) + .get() + .uri("/hi") + .exchange() + .expectBody(String.class) + .isEqualTo("Hi, Harold!"); + } + + @Test + public void resoleMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception { + this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + this.webClient.mutateWith(mockAuthentication(user)) + .get() + .uri("/hello") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("user"); + Authentication harold = new TestingAuthenticationToken("harold", "password", "ROLE_USER"); + this.webClient.mutateWith(mockAuthentication(harold)) + .get() + .uri("/hello") + .exchange() + .expectBody(String.class) + .isEqualTo("harold"); + } + + @Test + public void getWhenUsingObservationRegistryThenObservesRequest() { + this.spring.register(ObservationRegistryConfig.class).autowire(); + // @formatter:off + this.webClient + .get() + .uri("/hello") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isNotFound(); + // @formatter:on + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, times(6)).onStart(captor.capture()); + Iterator contexts = captor.getAllValues().iterator(); + assertThat(contexts.next().getContextualName()).isEqualTo("http get"); + assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain before"); + assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications"); + assertThat(contexts.next().getName()).isEqualTo("spring.security.authorizations"); + assertThat(contexts.next().getName()).isEqualTo("spring.security.http.secured.requests"); + assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain after"); + } + + // gh-16161 + @Test + public void getWhenUsingRSocketThenObservesRequest() { + this.spring.register(ObservationRegistryConfig.class, RSocketSecurityConfig.class).autowire(); + // @formatter:off + this.webClient + .get() + .uri("/hello") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isNotFound(); + // @formatter:on + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, times(6)).onStart(captor.capture()); + Iterator contexts = captor.getAllValues().iterator(); + assertThat(contexts.next().getContextualName()).isEqualTo("http get"); + assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain before"); + assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications"); + assertThat(contexts.next().getName()).isEqualTo("spring.security.authorizations"); + assertThat(contexts.next().getName()).isEqualTo("spring.security.http.secured.requests"); + assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain after"); + } + @Configuration static class SubclassConfig extends ServerHttpSecurityConfiguration { @@ -237,4 +358,120 @@ public Mono check(String password) { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @CurrentSecurityContext(expression = "authentication.{property}") + @interface CurrentAuthenticationProperty { + + String property(); + + } + + @RestController + static class TestController { + + @GetMapping("/hi") + String ifUser(@IsUser("harold") boolean isHarold) { + if (isHarold) { + return "Hi, Harold!"; + } + else { + return "Hi, Stranger!"; + } + } + + @GetMapping("/hello") + String getCurrentAuthenticationProperty( + @CurrentAuthenticationProperty(property = "principal") String principal) { + return principal; + } + + } + + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class MetaAnnotationPlaceholderConfig { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .httpBasic(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("user").password("password").authorities("app").build()); + } + + @Bean + TestController testController() { + return new TestController(); + } + + @Bean + AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + + } + + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class ObservationRegistryConfig { + + private ObservationHandler handler = mock(ObservationHandler.class); + + @Bean + SecurityWebFilterChain app(ServerHttpSecurity http) throws Exception { + http.httpBasic(withDefaults()).authorizeExchange((authorize) -> authorize.anyExchange().authenticated()); + return http.build(); + } + + @Bean + ReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withDefaultPasswordEncoder().username("user").password("password").authorities("app").build()); + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistry observationRegistry() { + given(this.handler.supportsContext(any())).willReturn(true); + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(this.handler); + return registry; + } + + } + + @EnableRSocketSecurity + static class RSocketSecurityConfig { + + @Bean + RSocketMessageHandler messageHandler() { + return new RSocketMessageHandler(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfigurationTests.java index d8be15886b2..2bb526c963a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java index a14986b1ee5..cf2d0358069 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java @@ -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. @@ -16,19 +16,30 @@ package org.springframework.security.config.annotation.web.socket; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -57,8 +68,11 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -90,8 +104,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken; public class WebSocketMessageBrokerSecurityConfigurationTests { @@ -164,6 +181,17 @@ public void addsAuthenticationPrincipalResolverWhenNoAuthorization() { .isEqualTo((String) this.messageUser.getPrincipal()); } + @Test + public void sendMessageWhenMetaAnnotationThenParsesExpression() { + loadConfig(NoInboundSecurityConfig.class); + this.messageUser = new TestingAuthenticationToken("harold", "password", "ROLE_USER"); + clientInboundChannel().send(message("/permitAll/hi")); + assertThat(this.context.getBean(MyController.class).message).isEqualTo("Hi, Harold!"); + this.messageUser = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + clientInboundChannel().send(message("/permitAll/hi")); + assertThat(this.context.getBean(MyController.class).message).isEqualTo("Hi, Stranger!"); + } + @Test public void addsCsrfProtectionWhenNoAuthorization() { loadConfig(NoInboundSecurityConfig.class); @@ -366,12 +394,55 @@ public void sendMessageWhenAnonymousConfiguredAndAnonymousUserThenPasses() { } @Test - public void sendMessageWhenAnonymousConfiguredAndLoggedInUserThenAccessDeniedException() { - loadConfig(WebSocketSecurityConfig.class); - assertThatExceptionOfType(MessageDeliveryException.class) - .isThrownBy(() -> clientInboundChannel().send(message("/anonymous"))) - .withCauseInstanceOf(AccessDeniedException.class); + public void sendMessageWhenObservationRegistryThenObserves() { + loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + Message message = message(headers, "/authenticated"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + clientInboundChannel().send(message); + ObservationHandler observationHandler = this.context.getBean(ObservationHandler.class); + verify(observationHandler).onStart(any()); + verify(observationHandler).onStop(any()); + headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + message = message(headers, "/denyAll"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + try { + clientInboundChannel().send(message); + } + catch (MessageDeliveryException ex) { + // okay + } + verify(observationHandler).onError(any()); + } + @Test + public void sendMessageWhenExcludeAuthorizationObservationsThenUnobserved() { + loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.class, SelectableObservationsConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + Message message = message(headers, "/authenticated"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + clientInboundChannel().send(message); + ObservationHandler observationHandler = this.context.getBean(ObservationHandler.class); + headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + message = message(headers, "/denyAll"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + try { + clientInboundChannel().send(message); + } + catch (MessageDeliveryException ex) { + // okay + } + verifyNoInteractions(observationHandler); + } + + // gh-16011 + @Test + public void enableWebSocketSecurityWhenWebSocketSecurityUsedThenAutowires() { + loadConfig(WithWebSecurity.class); } private void assertHandshake(HttpServletRequest request) { @@ -425,6 +496,7 @@ private T clientInboundChannel() { private void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); + this.context.setAllowBeanDefinitionOverriding(false); this.context.register(configs); this.context.setServletConfig(new MockServletConfig()); this.context.refresh(); @@ -585,6 +657,15 @@ TestHandshakeHandler testHandshakeHandler() { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + @Controller static class MyController { @@ -592,6 +673,8 @@ static class MyController { MyCustomArgument myCustomArgument; + String message; + @MessageMapping("/authentication") void authentication(@AuthenticationPrincipal String un) { this.authenticationPrincipal = un; @@ -602,6 +685,11 @@ void myCustom(MyCustomArgument myCustomArgument) { this.myCustomArgument = myCustomArgument; } + @MessageMapping("/hi") + void sayHello(@IsUser("harold") boolean isHarold) { + this.message = isHarold ? "Hi, Harold!" : "Hi, Stranger!"; + } + } static class MyCustomArgument { @@ -735,6 +823,11 @@ MyController myController() { return new MyController(); } + @Bean + AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + } @Configuration @@ -854,6 +947,13 @@ TestHandshakeHandler testHandshakeHandler() { } + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @Import(WebSocketSecurityConfig.class) + static class WithWebSecurity { + + } + @Configuration static class SyncExecutorConfig { @@ -864,4 +964,57 @@ static SyncExecutorSubscribableChannelPostProcessor postProcessor() { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index 2ac71e58266..dd70a6999c4 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -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. @@ -65,7 +65,7 @@ public class XsdDocumentedTests { String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.3.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.4.xsd"; XmlSupport xml = new XmlSupport(); @@ -151,8 +151,8 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() throws I .list((dir, name) -> name.endsWith(".xsd")); // @formatter:on assertThat(schemas.length) - .withFailMessage("the count is equal to 25, if not then schemaDocument needs updating") - .isEqualTo(25); + .withFailMessage("the count is equal to 26, if not then schemaDocument needs updating") + .isEqualTo(26); } /** diff --git a/config/src/test/java/org/springframework/security/config/http/AccessDeniedConfigTests.java b/config/src/test/java/org/springframework/security/config/http/AccessDeniedConfigTests.java index f38318f5eed..df43cc3049a 100644 --- a/config/src/test/java/org/springframework/security/config/http/AccessDeniedConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/AccessDeniedConfigTests.java @@ -18,13 +18,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -71,7 +71,7 @@ public void configureWhenAccessDeniedHandlerIsMissingLeadingSlashThenException() @WithMockUser public void configureWhenAccessDeniedHandlerRefThenAutowire() throws Exception { this.spring.configLocations(this.xml("AccessDeniedHandler")).autowire(); - this.mvc.perform(get("/")).andExpect(status().is(HttpStatus.GONE_410)); + this.mvc.perform(get("/")).andExpect(status().is(HttpStatus.GONE.value())); } @Test @@ -90,7 +90,7 @@ public static class GoneAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { - response.setStatus(HttpStatus.GONE_410); + response.setStatus(HttpStatus.GONE.value()); } } diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index 97bffee2f43..901945e73aa 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -22,12 +22,12 @@ import jakarta.servlet.Filter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.access.AccessDeniedException; @@ -566,7 +566,7 @@ private static class TeapotAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { - response.setStatus(HttpStatus.IM_A_TEAPOT_418); + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); } } diff --git a/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java b/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java index a20fe5397f1..a5b899db48c 100644 --- a/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java +++ b/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.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. @@ -113,6 +113,7 @@ public void validateCheckLoginPageIsntProtectedThrowsIllegalArgumentException() @Test public void validateCheckLoginPageAllowsAnonymous() { given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + given(this.authorizationManager.authorize(any(), any())).willCallRealMethod(); this.validator.validate(this.chainAuthorizationFilter); verify(this.logger).warn("Anonymous access to the login page doesn't appear to be enabled. " + "This is almost certainly an error. Please check your configuration allows unauthenticated " diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java index 86a1c351acb..79c785f5a37 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.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. @@ -53,35 +53,40 @@ public class FormLoginBeanDefinitionParserTests { @Test public void getLoginWhenAutoConfigThenShowsDefaultLoginPage() throws Exception { this.spring.configLocations(this.xml("Simple")).autowire(); - // @formatter:off - String expectedContent = "\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
    \n" - + "
    \n" - + " \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + " \n" - + "
    \n" - + "
    \n" - + ""; - // @formatter:on + String expectedContent = """ + + + + + + + + Please sign in + + + +
    + + + + +
    + + """; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } @@ -94,39 +99,43 @@ public void getLogoutWhenAutoConfigThenShowsDefaultLogoutPage() throws Exception @Test public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() throws Exception { this.spring.configLocations(this.xml("WithCustomAttributes")).autowire(); - // @formatter:off - String expectedContent = "\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
    \n" - + "
    \n" - + " \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + "

    \n" - + " \n" - + " \n" - + "

    \n" - + " \n" - + "
    \n" - + "
    \n" - + ""; - this.mvc.perform(get("/login")) - .andExpect(content().string(expectedContent)); - this.mvc.perform(get("/logout")) - .andExpect(status().is3xxRedirection()); - // @formatter:on + + String expectedContent = """ + + + + + + + + Please sign in + + + +
    + + + + +
    + + """; + this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); + this.mvc.perform(get("/logout")).andExpect(status().is3xxRedirection()); } @Test diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java index b8a86d2411c..9a4e3b041e1 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java @@ -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. @@ -91,6 +91,7 @@ public void getWhenUsingAuthorizationManagerThenRedirectsToLogin() throws Except AuthorizationManager authorizationManager = this.spring.getContext() .getBean(AuthorizationManager.class); given(authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + given(authorizationManager.authorize(any(), any())).willCallRealMethod(); // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index 57741cea04e..180bd2ec532 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -93,6 +93,7 @@ import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.SecurityContextHolderFilter; @@ -848,6 +849,7 @@ private void assertThatFiltersMatchExpectedAutoConfigList(String url) { assertThat(filters.next()).isInstanceOf(CsrfFilter.class); assertThat(filters.next()).isInstanceOf(LogoutFilter.class); assertThat(filters.next()).isInstanceOf(UsernamePasswordAuthenticationFilter.class); + assertThat(filters.next()).isInstanceOf(DefaultResourcesFilter.class); assertThat(filters.next()).isInstanceOf(DefaultLoginPageGeneratingFilter.class); assertThat(filters.next()).isInstanceOf(DefaultLogoutPageGeneratingFilter.class); assertThat(filters.next()).isInstanceOf(BasicAuthenticationFilter.class); diff --git a/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java b/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java index a4257f1732f..c71acbe5a44 100644 --- a/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java +++ b/config/src/test/java/org/springframework/security/config/method/InterceptMethodsBeanDefinitionDecoratorTests.java @@ -168,6 +168,7 @@ public void transactionalAuthorizationManagerMethodsShouldBeSecured() { @Test public void targetCustomAuthorizationManagerUsed() { + given(this.mockAuthorizationManager.authorize(any(), any())).willCallRealMethod(); given(this.mockAuthorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true)); this.targetCustomAuthorizationManager.doSomething(); verify(this.mockAuthorizationManager).check(any(), any()); diff --git a/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java b/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java new file mode 100644 index 00000000000..75dd6c28779 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.observation; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SecurityObservationSettings} + */ +public class SecurityObservationSettingsTests { + + @Test + void withDefaultsThenFilterOffAuthenticationOnAuthorizationOn() { + SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults().build(); + assertThat(defaults.shouldObserveRequests()).isFalse(); + assertThat(defaults.shouldObserveAuthentications()).isTrue(); + assertThat(defaults.shouldObserveAuthorizations()).isTrue(); + } + + @Test + void noObservationsWhenConstructedThenAllOff() { + SecurityObservationSettings defaults = SecurityObservationSettings.noObservations(); + assertThat(defaults.shouldObserveRequests()).isFalse(); + assertThat(defaults.shouldObserveAuthentications()).isFalse(); + assertThat(defaults.shouldObserveAuthorizations()).isFalse(); + } + + @Test + void withDefaultsWhenExclusionsThenInstanceReflects() { + SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults() + .shouldObserveAuthentications(false) + .shouldObserveAuthorizations(false) + .shouldObserveRequests(true) + .build(); + assertThat(defaults.shouldObserveRequests()).isTrue(); + assertThat(defaults.shouldObserveAuthentications()).isFalse(); + assertThat(defaults.shouldObserveAuthorizations()).isFalse(); + } + +} diff --git a/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java index 5a332582d30..68a6c22ab1c 100644 --- a/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java @@ -35,6 +35,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import static org.assertj.core.api.Assertions.assertThat; @@ -288,6 +289,32 @@ public void parseWhenRelayStateResolverThenUses() { verify(relayStateResolver).convert(request); } + @Test + public void parseWhenPlaceholdersThenResolves() throws Exception { + RelyingPartyRegistration sample = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + System.setProperty("registration-id", sample.getRegistrationId()); + System.setProperty("entity-id", sample.getEntityId()); + System.setProperty("acs-location", sample.getAssertionConsumerServiceLocation()); + System.setProperty("slo-location", sample.getSingleLogoutServiceLocation()); + System.setProperty("slo-response-location", sample.getSingleLogoutServiceResponseLocation()); + try (MockWebServer web = new MockWebServer()) { + web.start(); + String serverUrl = web.url("/metadata").toString(); + web.enqueue(xmlResponse(METADATA_RESPONSE)); + System.setProperty("metadata-location", serverUrl); + this.spring.configLocations(xml("PlaceholderRegistration")).autowire(); + } + RelyingPartyRegistration registration = this.relyingPartyRegistrationRepository + .findByRegistrationId(sample.getRegistrationId()); + assertThat(registration.getRegistrationId()).isEqualTo(sample.getRegistrationId()); + assertThat(registration.getEntityId()).isEqualTo(sample.getEntityId()); + assertThat(registration.getAssertionConsumerServiceLocation()) + .isEqualTo(sample.getAssertionConsumerServiceLocation()); + assertThat(registration.getSingleLogoutServiceLocation()).isEqualTo(sample.getSingleLogoutServiceLocation()); + assertThat(registration.getSingleLogoutServiceResponseLocation()) + .isEqualTo(sample.getSingleLogoutServiceResponseLocation()); + } + private static MockResponse xmlResponse(String xml) { return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).setBody(xml); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java index 07d22be6172..63646400efd 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -29,7 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; -import org.springframework.core.ResolvableType; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.test.web.reactive.server.FluxExchangeResult; @@ -51,7 +51,6 @@ public class CorsSpecTests { @Mock private CorsConfigurationSource source; - @Mock private ApplicationContext context; ServerHttpSecurity http; @@ -62,6 +61,8 @@ public class CorsSpecTests { @BeforeEach public void setup() { + this.context = new GenericApplicationContext(); + ((GenericApplicationContext) this.context).refresh(); this.http = new TestingServerHttpSecurity().applicationContext(this.context); } @@ -92,9 +93,7 @@ public void corsWhenEnabledInLambdaThenAccessControlAllowOriginAndSecurityHeader @Test public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSecurityHeaders() { givenGetCorsConfigurationWillReturnWildcard(); - given(this.context.getBeanNamesForType(any(ResolvableType.class))).willReturn(new String[] { "source" }, - new String[0]); - given(this.context.getBean("source")).willReturn(this.source); + ((GenericApplicationContext) this.context).registerBean(CorsConfigurationSource.class, () -> this.source); this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); this.expectedHeaders.set("X-Frame-Options", "DENY"); assertHeaders(); @@ -102,7 +101,6 @@ public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSe @Test public void corsWhenNoConfigurationSourceThenNoCorsHeaders() { - given(this.context.getBeanNamesForType(any(ResolvableType.class))).willReturn(new String[0]); this.headerNamesNotPresent.add("Access-Control-Allow-Origin"); assertHeaders(); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java index d348d95f8af..0bd8391d71d 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.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. @@ -17,9 +17,11 @@ package org.springframework.security.config.web.server; import java.net.URI; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; @@ -31,9 +33,12 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -41,8 +46,10 @@ import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; @@ -59,7 +66,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.server.ServerWebExchange; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -215,6 +224,62 @@ public void oauth2ClientWhenCustomObjectsInLambdaThenUsed() { verify(requestCache).getRedirectUri(any()); } + @Test + @SuppressWarnings("unchecked") + public void oauth2ClientWhenCustomAccessTokenResponseClientThenUsed() { + this.spring.register(OAuth2ClientBeanConfig.class, AuthorizedClientController.class).autowire(); + ReactiveClientRegistrationRepository clientRegistrationRepository = this.spring.getContext() + .getBean(ReactiveClientRegistrationRepository.class); + given(clientRegistrationRepository.findByRegistrationId(any())).willReturn(Mono.just(this.registration)); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext() + .getBean(ServerOAuth2AuthorizedClientRepository.class); + given(authorizedClientRepository.saveAuthorizedClient(any(OAuth2AuthorizedClient.class), + any(Authentication.class), any(ServerWebExchange.class))) + .willReturn(Mono.empty()); + ServerAuthorizationRequestRepository authorizationRequestRepository = this.spring + .getContext() + .getBean(ServerAuthorizationRequestRepository.class); + OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() + .redirectUri("/authorize/oauth2/code/registration-id") + .build(); + given(authorizationRequestRepository.loadAuthorizationRequest(any(ServerWebExchange.class))) + .willReturn(Mono.just(authorizationRequest)); + given(authorizationRequestRepository.removeAuthorizationRequest(any(ServerWebExchange.class))) + .willReturn(Mono.just(authorizationRequest)); + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = this.spring + .getContext() + .getBean(ReactiveOAuth2AccessTokenResponseClient.class); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .scopes(Set.of()) + .expiresIn(300) + .build(); + given(accessTokenResponseClient.getTokenResponse(any(OAuth2AuthorizationCodeGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + // @formatter:off + this.client.get() + .uri((uriBuilder) -> uriBuilder + .path("/authorize/oauth2/code/registration-id") + .queryParam(OAuth2ParameterNames.CODE, "code") + .queryParam(OAuth2ParameterNames.STATE, "state") + .build() + ) + .exchange() + .expectStatus().is3xxRedirection(); + // @formatter:on + ArgumentCaptor grantRequestArgumentCaptor = ArgumentCaptor + .forClass(OAuth2AuthorizationCodeGrantRequest.class); + verify(accessTokenResponseClient).getTokenResponse(grantRequestArgumentCaptor.capture()); + OAuth2AuthorizationCodeGrantRequest grantRequest = grantRequestArgumentCaptor.getValue(); + assertThat(grantRequest.getClientRegistration()).isEqualTo(this.registration); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationRequest()).isEqualTo(authorizationRequest); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode()).isEqualTo("code"); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationResponse().getState()).isEqualTo("state"); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationResponse().getRedirectUri()) + .startsWith("/authorize/oauth2/code/registration-id"); + } + @Configuration @EnableWebFlux @EnableWebFluxSecurity @@ -324,4 +389,44 @@ SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { } + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class OAuth2ClientBeanConfig { + + @Bean + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + http + .oauth2Client((oauth2Client) -> oauth2Client + .authorizationRequestRepository(authorizationRequestRepository()) + ); + // @formatter:on + return http.build(); + } + + @Bean + @SuppressWarnings("unchecked") + ServerAuthorizationRequestRepository authorizationRequestRepository() { + return mock(ServerAuthorizationRequestRepository.class); + } + + @Bean + @SuppressWarnings("unchecked") + ReactiveOAuth2AccessTokenResponseClient authorizationCodeAccessTokenResponseClient() { + return mock(ReactiveOAuth2AccessTokenResponseClient.class); + } + + @Bean + ReactiveClientRegistrationRepository clientRegistrationRepository() { + return mock(ReactiveClientRegistrationRepository.class); + } + + @Bean + ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { + return mock(ServerOAuth2AuthorizedClientRepository.class); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index ec686bf8514..4597d3e7865 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -31,6 +31,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; @@ -63,7 +64,10 @@ import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; @@ -82,6 +86,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.TestOAuth2Users; import org.springframework.security.oauth2.jwt.Jwt; @@ -256,6 +261,65 @@ public void defaultLoginPageWithOAuth2LoginHttpBasicAndXhrRequestThenUnauthorize // @formatter:on } + @Test + public void defaultLoginPageWhenCustomLoginPageThenGeneratedLoginPageDoesNotExist() { + this.spring + .register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/login") + .exchange() + .expectStatus().isNotFound(); + // @formatter:on + } + + @Test + public void oauth2LoginWhenCustomLoginPageAndSingleClientRegistrationThenRedirectsToLoginPage() { + this.spring + .register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/login"); + // @formatter:on + } + + @Test + public void oauth2LoginWhenCustomLoginPageAndMultipleClientRegistrationsThenRedirectsToLoginPage() { + this.spring + .register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/login"); + // @formatter:on + } + + @Test + public void oauth2LoginWhenProviderLoginPageAndMultipleClientRegistrationsThenRedirectsToProvider() { + this.spring + .register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithProviderLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/oauth2/authorization/github"); + // @formatter:on + } + @Test public void oauth2AuthorizeWhenCustomObjectsThenUsed() { this.spring @@ -457,6 +521,7 @@ public void oauth2LoginWhenCustomBeansThenUsed() { OidcUser user = TestOidcUsers.create(); ReactiveOAuth2UserService userService = config.userService; given(userService.loadUser(any())).willReturn(Mono.just(user)); + ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver = config.authorizationRequestResolver; // @formatter:off webTestClient.get() .uri("/login/oauth2/code/google") @@ -466,6 +531,7 @@ public void oauth2LoginWhenCustomBeansThenUsed() { verify(config.jwtDecoderFactory).createDecoder(any()); verify(tokenResponseClient).getTokenResponse(any()); verify(securityContextRepository).save(any(), any()); + verify(authorizationRequestResolver).resolve(any()); } // gh-5562 @@ -601,6 +667,41 @@ public void oauth2LoginWhenDefaultsThenNoOidcSessionRegistry() { .block()).isEmpty(); } + @Test + public void oauth2LoginWhenOauth2UserServiceBeanPresent() { + this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithOauth2UserService.class) + .autowire(); + WebTestClient webTestClient = WebTestClientBuilder.bindToWebFilters(this.springSecurity).build(); + OAuth2LoginWithOauth2UserService config = this.spring.getContext() + .getBean(OAuth2LoginWithOauth2UserService.class); + OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build(); + OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build(); + OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response); + OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid"); + OAuth2AuthorizationCodeAuthenticationToken token = new OAuth2AuthorizationCodeAuthenticationToken(google, + exchange, accessToken); + ServerAuthenticationConverter converter = config.authenticationConverter; + given(converter.convert(any())).willReturn(Mono.just(token)); + ServerSecurityContextRepository securityContextRepository = config.securityContextRepository; + given(securityContextRepository.save(any(), any())).willReturn(Mono.empty()); + given(securityContextRepository.load(any())).willReturn(authentication(token)); + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) + .tokenType(accessToken.getTokenType()) + .scopes(accessToken.getScopes()) + .additionalParameters(additionalParameters) + .build(); + ReactiveOAuth2AccessTokenResponseClient tokenResponseClient = config.tokenResponseClient; + given(tokenResponseClient.getTokenResponse(any())).willReturn(Mono.just(accessTokenResponse)); + ReactiveOAuth2UserService userService = config.reactiveOAuth2UserService; + given(userService.loadUser(any())).willReturn(Mono + .just(new DefaultOAuth2User(AuthorityUtils.createAuthorityList("USER"), Map.of("sub", "subject"), "sub"))); + webTestClient.get().uri("/login/oauth2/code/google").exchange().expectStatus().is3xxRedirection(); + verify(userService).loadUser(any()); + + } + Mono authentication(Authentication authentication) { SecurityContext context = new SecurityContextImpl(); context.setAuthentication(authentication); @@ -611,6 +712,51 @@ T getBean(Class beanClass) { return this.spring.getContext().getBean(beanClass); } + @Configuration + static class OAuth2LoginWithOauth2UserService { + + ReactiveOAuth2AccessTokenResponseClient tokenResponseClient = mock( + ReactiveOAuth2AccessTokenResponseClient.class); + + ReactiveOAuth2UserService reactiveOAuth2UserService = mock( + DefaultReactiveOAuth2UserService.class); + + ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class); + + ServerSecurityContextRepository securityContextRepository = mock(ServerSecurityContextRepository.class); + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + http.authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login((c) -> c.authenticationConverter(this.authenticationConverter) + .securityContextRepository(this.securityContextRepository)); + return http.build(); + } + + @Bean + ReactiveOAuth2UserService customOAuth2UserService() { + return this.reactiveOAuth2UserService; + } + + @Bean + ReactiveJwtDecoderFactory jwtDecoderFactory() { + return (clientRegistration) -> (token) -> { + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.SUB, "subject"); + claims.put(IdTokenClaimNames.ISS, "http://localhost/issuer"); + claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client")); + claims.put(IdTokenClaimNames.AZP, "client"); + return Mono.just(TestJwts.jwt().claims((c) -> c.putAll(claims)).build()); + }; + } + + @Bean + ReactiveOAuth2AccessTokenResponseClient requestReactiveOAuth2AccessTokenResponseClient() { + return this.tokenResponseClient; + } + + } + @Configuration @EnableWebFluxSecurity static class OAuth2LoginWithMultipleClientRegistrations { @@ -753,6 +899,46 @@ SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { } + @Configuration + @EnableWebFluxSecurity + static class OAuth2LoginWithCustomLoginPage { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize + .pathMatchers(HttpMethod.GET, "/login").permitAll() + .anyExchange().authenticated() + ) + .oauth2Login((oauth2) -> oauth2 + .loginPage("/login") + ); + // @formatter:on + return http.build(); + } + + } + + @Configuration + @EnableWebFluxSecurity + static class OAuth2LoginWithProviderLoginPage { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http.authorizeExchange((authorize) -> authorize + .anyExchange().authenticated() + ) + .oauth2Login((oauth2) -> oauth2 + .loginPage("/oauth2/authorization/github") + ); + // @formatter:on + return http.build(); + } + + } + @Configuration static class OAuth2LoginMockAuthenticationManagerConfig { @@ -837,6 +1023,10 @@ static class OAuth2LoginWithCustomBeansConfig { ServerSecurityContextRepository securityContextRepository = mock(ServerSecurityContextRepository.class); + ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver = spy( + new DefaultServerOAuth2AuthorizationRequestResolver(new InMemoryReactiveClientRegistrationRepository( + TestClientRegistrations.clientRegistration().build()))); + @Bean SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { // @formatter:off @@ -864,6 +1054,11 @@ ReactiveJwtDecoderFactory jwtDecoderFactory() { return this.jwtDecoderFactory; } + @Bean + ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() { + return this.authorizationRequestResolver; + } + @Bean ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient() { return this.tokenResponseClient; diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java index 37190608c1d..a8494bdc909 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java @@ -19,6 +19,10 @@ import org.junit.jupiter.api.Test; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens; +import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry; +import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import static org.assertj.core.api.Assertions.assertThat; @@ -27,36 +31,43 @@ */ public class OidcBackChannelServerLogoutHandlerTests { + private final ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry(); + + private final OidcBackChannelLogoutAuthentication token = new OidcBackChannelLogoutAuthentication( + TestOidcLogoutTokens.withSubject("issuer", "subject").build(), + TestClientRegistrations.clientRegistration().build()); + // gh-14553 @Test public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() { - OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); + OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry); + logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout"); MockServerHttpRequest request = MockServerHttpRequest .get("https://host.docker.internal:8090/back-channel/logout") .build(); - String endpoint = logoutHandler.computeLogoutEndpoint(request); - assertThat(endpoint).isEqualTo("https://localhost:8090/logout"); + String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token); + assertThat(endpoint).startsWith("https://localhost:8090/logout"); } @Test public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() { - OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); + OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry); logoutHandler.setLogoutUri("{baseUrl}/logout"); MockServerHttpRequest request = MockServerHttpRequest .get("http://host.docker.internal:8090/back-channel/logout") .build(); - String endpoint = logoutHandler.computeLogoutEndpoint(request); - assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout"); + String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token); + assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout"); } // gh-14609 @Test public void computeLogoutEndpointWhenLogoutUriThenUses() { - OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); + OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry); logoutHandler.setLogoutUri("http://localhost:8090/logout"); MockServerHttpRequest request = MockServerHttpRequest.get("https://server-one.com/back-channel/logout").build(); - String endpoint = logoutHandler.computeLogoutEndpoint(request); - assertThat(endpoint).isEqualTo("http://localhost:8090/logout"); + String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token); + assertThat(endpoint).startsWith("http://localhost:8090/logout"); } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java index 461f7ac47e6..ebff5d102b8 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java @@ -25,8 +25,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; -import com.gargoylesoftware.htmlunit.util.UrlUtils; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; @@ -40,6 +40,7 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.htmlunit.util.UrlUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Mono; @@ -99,6 +100,7 @@ import org.springframework.web.server.WebSession; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasValue; import static org.mockito.ArgumentMatchers.any; @@ -276,6 +278,52 @@ void logoutWhenRemoteLogoutUriThenUses() { this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk(); } + @Test + void logoutWhenSelfRemoteLogoutUriThenUses() { + this.spring.register(WebServerConfig.class, OidcProviderConfig.class, SelfLogoutUriConfig.class).autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + String sessionId = login(); + String logoutToken = this.test.get() + .uri("/token/logout") + .cookie("SESSION", sessionId) + .exchange() + .expectStatus() + .isOk() + .returnResult(String.class) + .getResponseBody() + .blockFirst(); + this.test.post() + .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .body(BodyInserters.fromFormData("logout_token", logoutToken)) + .exchange() + .expectStatus() + .isOk(); + this.test.get().uri("/token/logout").cookie("SESSION", sessionId).exchange().expectStatus().isUnauthorized(); + } + + @Test + void logoutWhenDifferentCookieNameThenUses() { + this.spring.register(OidcProviderConfig.class, CookieConfig.class).autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + String sessionId = login(); + String logoutToken = this.test.get() + .uri("/token/logout") + .cookie("SESSION", sessionId) + .exchange() + .expectStatus() + .isOk() + .returnResult(String.class) + .getResponseBody() + .blockFirst(); + this.test.post() + .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .body(BodyInserters.fromFormData("logout_token", logoutToken)) + .exchange() + .expectStatus() + .isOk(); + this.test.get().uri("/token/logout").cookie("SESSION", sessionId).exchange().expectStatus().isUnauthorized(); + } + @Test void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() { this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire(); @@ -476,6 +524,83 @@ SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { } + @Configuration + @EnableWebFluxSecurity + @Import(RegistrationConfig.class) + static class SelfLogoutUriConfig { + + @Bean + @Order(1) + SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc + .backChannel(Customizer.withDefaults()) + ); + // @formatter:on + + return http.build(); + } + + } + + @Configuration + @EnableWebFluxSecurity + @Import(RegistrationConfig.class) + static class CookieConfig { + + private final MockWebServer server = new MockWebServer(); + + @Bean + @Order(1) + SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc + .backChannel(Customizer.withDefaults()) + ); + // @formatter:on + + return http.build(); + } + + @Bean + ReactiveOidcSessionRegistry oidcSessionRegistry() { + return new InMemoryReactiveOidcSessionRegistry(); + } + + @Bean + OidcBackChannelServerLogoutHandler oidcLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) { + OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(sessionRegistry); + logoutHandler.setSessionCookieName("JSESSIONID"); + return logoutHandler; + } + + @Bean + MockWebServer web(ObjectProvider web) { + WebTestClientDispatcher dispatcher = new WebTestClientDispatcher(web); + dispatcher.setAssertion((rr) -> { + String cookie = rr.getHeaders().get("Cookie"); + if (cookie == null) { + return; + } + assertThat(cookie).contains("JSESSIONID"); + }); + this.server.setDispatcher(dispatcher); + return this.server; + } + + @PreDestroy + void shutdown() throws IOException { + this.server.shutdown(); + } + + } + @Configuration @EnableWebFluxSecurity @Import(RegistrationConfig.class) @@ -489,7 +614,7 @@ SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { // @formatter:off http .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) - .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry)) + .oauth2Login(Customizer.withDefaults()) .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); // @formatter:on @@ -744,12 +869,15 @@ private static class WebTestClientDispatcher extends Dispatcher { private WebTestClient web; + private Consumer assertion = (rr) -> { }; + WebTestClientDispatcher(ObjectProvider web) { this.webProvider = web; } @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + this.assertion.accept(request); this.web = this.webProvider.getObject(); String method = request.getMethod(); String path = request.getPath(); @@ -792,6 +920,10 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio } } + void setAssertion(Consumer assertion) { + this.assertion = assertion; + } + private String session(RecordedRequest request) { String cookieHeaderValue = request.getHeader("Cookie"); if (cookieHeaderValue == null) { @@ -803,6 +935,9 @@ private String session(RecordedRequest request) { if (SESSION_COOKIE_NAME.equals(parts[0])) { return parts[1]; } + if ("JSESSIONID".equals(parts[0])) { + return parts[1]; + } } return null; } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java new file mode 100644 index 00000000000..c19f330eba4 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java @@ -0,0 +1,411 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.ott.OneTimeToken; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler; +import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; + +/** + * Tests for {@link ServerHttpSecurity.OneTimeTokenLoginSpec} + * + * @author Max Batischev + */ +@ExtendWith(SpringTestContextExtension.class) +public class OneTimeTokenLoginSpecTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + private WebTestClient client; + + private static final String EXPECTED_HTML_HEAD = """ + + + + + + + + Please sign in + + + """; + + private static final String LOGIN_PART = """ +