Compare commits

..

140 Commits

Author SHA1 Message Date
Joe Grandja
12997b6ab6 Polish oauth2-client tests with missing Content-Type header 2026-03-30 13:40:32 -04:00
Rob Winch
8c4cfe83f8
Merge pull request #19006 from rwinch/main-CredentialRecordOwnerAuthorizationManager
Merge Add CredentialRecordOwnerAuthorizationManager
2026-03-29 23:45:21 -04:00
Robert Winch
9d047b6edc
Merge CredentialRecordOwnerAuthorizationManager 2026-03-29 22:24:52 -05:00
Robert Winch
c08329c0c5
Merge CredentialRecordOwnerAuthorizationManager 2026-03-29 22:24:21 -05:00
dependabot[bot]
875b076c39 Bump tools.jackson:jackson-bom from 3.1.0 to 3.1.1
Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.1.0 to 3.1.1.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.1.0...jackson-bom-3.1.1)

---
updated-dependencies:
- dependency-name: tools.jackson:jackson-bom
  dependency-version: 3.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 03:19:13 +00:00
dependabot[bot]
c2441e5a58 Bump com.nimbusds:oauth2-oidc-sdk from 11.35 to 11.37
Bumps [com.nimbusds:oauth2-oidc-sdk](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions) from 11.35 to 11.37.
- [Changelog](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/branches/compare/11.37..11.35)

---
updated-dependencies:
- dependency-name: com.nimbusds:oauth2-oidc-sdk
  dependency-version: '11.37'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 03:18:42 +00:00
Robert Winch
a856baa6a8
Add CredentialRecordOwnerAuthorizationManager
Add CredentialRecordOwnerAuthorizationManager that verifies the
credential being deleted is owned by the currently authenticated user.
Also add an AuthorizationManager<Bytes> to WebAuthnRegistrationFilter
for the delete credential operation, defaulting to deny all, and wire it
up in WebAuthnConfigurer.

Per the WebAuthn specification [1], credential ids contain at least 16
bytes with at least 100 bits of entropy, making them practically
unguessable. The specification also advises that credential ids should
be kept private, as exposing them can leak personally identifying
information [2]. The CredentialRecordOwnerAuthorizationManager serves as
defense in depth: even if a credential id were somehow exposed, an
unauthorized user could not delete another user's credential.

[1] https://www.w3.org/TR/webauthn-3/#credential-id
[2] https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak
2026-03-29 21:54:27 -05:00
Josh Cummings
036326d70b Merge branch '7.0.x' 2026-03-27 16:49:33 -06:00
Josh Cummings
611786e4b5 Merge branch '6.5.x' into 7.0.x 2026-03-27 16:49:26 -06:00
Josh Cummings
ac63cf4fa5 Polish CustomAuthorizationManager Docs
Issue gh-13967

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 16:45:25 -06:00
as1605
f6bb55effb Fix documentation for Custom Authorization Manager
Closes gh-13967

Signed-off-by: as1605 <1605.aditya.singh@gmail.com>
2026-03-27 16:45:25 -06:00
Josh Cummings
c489136515 Merge branch '7.0.x' 2026-03-27 16:40:04 -06:00
Josh Cummings
6020ab8e65 Polish CustomAuthorizationManager Docs
Issue gh-13967

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 16:36:00 -06:00
as1605
3076367168 Fix documentation for Custom Authorization Manager
Closes gh-13967

Signed-off-by: as1605 <1605.aditya.singh@gmail.com>
2026-03-27 16:36:00 -06:00
Josh Cummings
2c32a9a969 Merge branch '7.0.x' 2026-03-27 16:10:36 -06:00
Josh Cummings
721b22d87a Merge remote-tracking branch 'origin/6.5.x' into 7.0.x 2026-03-27 16:10:18 -06:00
Tran Ngoc Nhan
85b756cb74 Update FilterChainProxy#getFilters(String) javadoc
Closes gh-18157

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-27 16:09:50 -06:00
Andrey Litvitski
b92c072501 add tests
Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-03-27 15:26:57 -06:00
Andrey Litvitski
6335caabae polish
Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-03-27 15:26:57 -06:00
Andrey Litvitski
c3e0b98b7e Use idiomatic Kotlin in custom filter documentation
This will make Kotlin and all users more native and readable.

Closes: gh-18967

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-03-27 15:26:57 -06:00
Ziqin Wang
acbf64a47d Improve And/Or-RequestMatcher/ServerWebExchangeMatcher API
Currently, the List-receiving constructors of AndRequestMatcher,
OrRequestMatcher, AndServerWebExchangeMatcher, and OrServerWebExchangeMatcher
don't support covariance, which adds obstacles to users of these
APIs.  For example, one cannot pass a List<PathPatternRequestMatcher>
to OrRequestMatcher(List<RequestMatcher>).

This commit resolves the aforementioned problem.  It should not
break existing code.

Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
2026-03-27 15:24:55 -06:00
Joe Kuhel
46e27aa693 Remove compiler warnings in spring-security-web
- fix compiler warnings in ServerOneTimeTokenAuthenticationConverter
- Replace deprecated API calls to create a OneTimeTokenAuthenticationToken.unauthenticated with OneTimeTokenAuthenticationToken(String token) call
- Update HttpMessageConverterAuthenticationSuccessHandler to replace deprecated MappingJackson2HttpMessageConverter with JacksonJsonHttpMessageConverter
- Replace updated OneTimeTokenAuthenticationConverter to use non-deprecated OneTimeTokenAuthenticationToken constructor
- update tests to remove use of deprecated methods
- refactor JdbcTokenRepositoryImpl to remove extension of deprecated JdbcDaoSupport class
- enable compile-warnings-error plugin

Closes gh-18441

Signed-off-by: Joe Kuhel <4983938+jkuhel@users.noreply.github.com>
2026-03-27 15:14:55 -06:00
dependabot[bot]
441e0fc976 Bump org.apereo.cas.client:cas-client-core from 4.0.4 to 4.1.0
Bumps [org.apereo.cas.client:cas-client-core](https://github.com/apereo/java-cas-client) from 4.0.4 to 4.1.0.
- [Release notes](https://github.com/apereo/java-cas-client/releases)
- [Commits](https://github.com/apereo/java-cas-client/compare/cas-client-4.0.4...cas-client-4.1.0)

---
updated-dependencies:
- dependency-name: org.apereo.cas.client:cas-client-core
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 19:45:26 +00:00
Josh Cummings
41efee0d35 Merge branch '7.0.x' 2026-03-27 13:27:15 -06:00
Josh Cummings
0ce76d2c5d Merge branch '6.5.x' into 7.0.x 2026-03-27 13:27:03 -06:00
dependabot[bot]
66cf02c6b0 Bump spring-io/spring-gradle-build-action from 2.0.5 to 2.0.6
Bumps [spring-io/spring-gradle-build-action](https://github.com/spring-io/spring-gradle-build-action) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/spring-io/spring-gradle-build-action/releases)
- [Commits](efc55f07f4...c8668747d7)

---
updated-dependencies:
- dependency-name: spring-io/spring-gradle-build-action
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:26:10 -06:00
dependabot[bot]
7441ce7f16 Bump spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/perform-release.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:25:46 -06:00
dependabot[bot]
9dbcd8cf00 Bump spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:25:35 -06:00
Josh Cummings
63e0d66811 Merge branch '7.0.x' 2026-03-27 13:23:08 -06:00
Josh Cummings
e6db4418b0 Merge branch '6.5.x' into 7.0.x 2026-03-27 13:22:44 -06:00
Josh Cummings
835d6c1fbd Add Issuer Validation to withIssuerLocation Snippets
Closes gh-19000

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 13:22:24 -06:00
Josh Cummings
95b6dc753a
Merge branch '7.0.x' 2026-03-27 12:14:47 -06:00
Josh Cummings
9fb3e14989
Merge branch '6.5.x' into 7.0.x 2026-03-27 12:14:41 -06:00
Josh Cummings
fc90a1ffeb
Merge branch '7.0.x' 2026-03-27 12:13:54 -06:00
Josh Cummings
de14d9684f
Add Reference Docs for DelegatingJwtGrantedAuthoritiesConverter
Issue gh-18300

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 12:13:49 -06:00
Josh Cummings
2c90edd7b7
Merge branch '6.5.x' into 7.0.x 2026-03-27 12:12:27 -06:00
Josh Cummings
95b2cdf7f4
Clarify JavaDoc
Removed note about DelegatingJwtGrantedAuthoritiesConverter from
ExpressionJwtGrantedAuthoritiesConverter and further explained in
DelegatingJwtGrantedAuthoritiesConverter where it comes in handy.

Issue gh-18300

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 11:48:56 -06:00
dependabot[bot]
d5d466b0eb Bump org.jetbrains.dokka from 2.1.0 to 2.2.0
Bumps [org.jetbrains.dokka](https://github.com/Kotlin/dokka) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/Kotlin/dokka/releases)
- [Commits](https://github.com/Kotlin/dokka/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.dokka
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 03:17:45 +00:00
dependabot[bot]
2970d2baf9 Bump org.jetbrains.dokka:dokka-gradle-plugin from 2.1.0 to 2.2.0
Bumps [org.jetbrains.dokka:dokka-gradle-plugin](https://github.com/Kotlin/dokka) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/Kotlin/dokka/releases)
- [Commits](https://github.com/Kotlin/dokka/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.dokka:dokka-gradle-plugin
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 03:17:37 +00:00
dependabot[bot]
826f5d6d72 Bump spring-io/spring-gradle-build-action from 2.0.5 to 2.0.6
Bumps [spring-io/spring-gradle-build-action](https://github.com/spring-io/spring-gradle-build-action) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/spring-io/spring-gradle-build-action/releases)
- [Commits](efc55f07f4...c8668747d7)

---
updated-dependencies:
- dependency-name: spring-io/spring-gradle-build-action
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 00:46:59 +00:00
Rob Winch
f0e71a8bc4
Merge pull request #18990 from rwinch/7.0.x-gh-18970-null-oncommitted
Merge Handle null value in OnCommittedResponseWrapper header methods
2026-03-26 17:33:33 -04:00
Rob Winch
4704aea72a
Merge pull request #18991 from rwinch/main-gh-18970-null-oncommitted
Merge Handle null value in OnCommittedResponseWrapper header methods
2026-03-26 17:31:43 -04:00
Rob Winch
3ecf84855e
Merge pull request #18989 from rwinch/gh-18970-null-oncommitted
Merge Handle null value in OnCommittedResponseWrapper header methods
2026-03-26 17:29:33 -04:00
Robert Winch
9f67afee42
Merge Handle null value in OnCommittedResponseWrapper header methods 2026-03-26 15:58:12 -05:00
Robert Winch
2848b95fe0
Merge Handle null value in OnCommittedResponseWrapper header methods 2026-03-26 15:44:49 -05:00
Robert Winch
0039bc0cf0
Handle null value in OnCommittedResponseWrapper header methods
Closes gh-18970
2026-03-26 14:50:44 -05:00
dependabot[bot]
aff736903d Bump picomatch from 2.3.1 to 2.3.2 in /javascript
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 21:48:06 +00:00
Josh Cummings
0606ff152b Merge branch '7.0.x' 2026-03-25 15:20:07 -06:00
Josh Cummings
671a53e850 Merge branch '6.5.x' into 7.0.x 2026-03-25 15:19:59 -06:00
Josh Cummings
057e5181ea Adjust Formatting
Issue gh-18805

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-25 15:19:29 -06:00
Tran Ngoc Nhan
178ca56aaf Fallback defaultTargetUrl if refererHeader is empty
Closes gh-18805

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-25 15:19:29 -06:00
Josh Cummings
a80447c65f Merge branch '7.0.x' 2026-03-25 15:11:59 -06:00
Josh Cummings
164fbaf007 Merge branch '6.5.x' into 7.0.x 2026-03-25 15:11:52 -06:00
dependabot[bot]
61ccf14953 Bump org.hibernate.orm:hibernate-core from 6.6.44.Final to 6.6.45.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.44.Final to 6.6.45.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.45/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.44...6.6.45)

---
updated-dependencies:
- dependency-name: org.hibernate.orm:hibernate-core
  dependency-version: 6.6.45.Final
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 15:11:27 -06:00
Josh Cummings
608b36bb1d Add docs-build to Dependabot Auto-Merge
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-25 14:45:37 -06:00
Robert Winch
51ce11cbd2
Move InetAddressMatcher to spring-security-core
Closes gh-18979
2026-03-25 15:25:57 -05:00
Robert Winch
c6e60c84f9 Add subsections to cors
This helps make the docs look more uniform after adding
PreFlightRequestFilter docs in its own section

Issue gh-18926
2026-03-25 16:04:42 -04:00
Robert Winch
4199240662 Add Support for PreFlightRequestFilter
Closes gh-18926
2026-03-25 16:04:42 -04:00
Robert Winch
0ef8a4ff27 Update to Spring Framework 7.0.7-SNAPSHOT
Necessary to pick up Spring Framework's `PreFlightRequestFilter`

Issue gh-18926
2026-03-25 16:04:42 -04:00
Josh Cummings
c749ead5f1 Publish KDoc for the Kotlin DSL
Applies Dokka to any subproject using security-kotlin via DocsPlugin,
aggregates KDoc alongside Javadoc in syncAntoraAttachments, and adds
a Kotlin API entry to the reference docs navigation.

Closes gh-18968

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-25 13:58:38 -06:00
Josh Cummings
622f75d346 Move Antora Tasks into DocsPlugin
All Spring projects using io.spring.convention.docs are also using Antora,
so these tasks belong in the convention rather than each project's build script.

Issue gh-18968
2026-03-25 13:58:38 -06:00
Joe Grandja
db67f36492 Fix ID Token auth_time validation (reactive)
Issue gh-18839 gh-17246
2026-03-25 14:28:00 -04:00
Joe Grandja
a8281a9c62 Merge branch '7.0.x' 2026-03-25 13:23:11 -04:00
Joe Grandja
65cf2586c5 Merge branch '6.5.x' into 7.0.x
Closes gh-18978
2026-03-25 12:40:43 -04:00
Joe Grandja
6e683f2286 Fix ID Token auth_time validation
Closes gh-18839
2026-03-25 11:33:55 -04:00
dependabot[bot]
f6f3b697fe Bump com.nimbusds:oauth2-oidc-sdk from 11.34 to 11.35
Bumps [com.nimbusds:oauth2-oidc-sdk](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions) from 11.34 to 11.35.
- [Changelog](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/branches/compare/11.35..11.34)

---
updated-dependencies:
- dependency-name: com.nimbusds:oauth2-oidc-sdk
  dependency-version: '11.35'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 03:17:16 +00:00
Josh Cummings
2c2666065f
Merge branch '7.0.x' 2026-03-24 13:39:37 -06:00
Josh Cummings
bae4cdd765
Adjust for Nullability
Issue gh-18973

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-24 13:39:27 -06:00
Josh Cummings
2a8976f2f0 Merge branch '7.0.x' 2026-03-23 18:13:15 -06:00
Josh Cummings
a7c3e842d6 Merge branch '6.5.x' into 7.0.x 2026-03-23 18:12:36 -06:00
Josh Cummings
b6e24db68c Return Mono.empty on Empty POST
Closes gh-18973

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-23 18:12:21 -06:00
Josh Cummings
5100bf3db9 Merge branch '7.0.x' 2026-03-23 17:53:41 -06:00
Josh Cummings
7dea8b8ca2 Merge branch '6.5.x' into 7.0.x 2026-03-23 17:53:14 -06:00
Daniel Garnier-Moiroux
aeb5fc1fb0 Fix HttpSessionRequestCache#getMatchingRequest query string parsing
- URL parsing changed in framework 6.2, and fails when path contains a % sign.
- The HttpSessionRequestCache only needs to inspect the query string, not the full URL.

Fixes gh-16656

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-03-23 17:52:17 -06:00
Eric Haag
91199e7202 Gracefully handle detached HEAD in branch version check
Previously, the `CheckExpectedBranchVersionPlugin` would crash the Gradle
configuration phase if the project was in a detached HEAD state or not
in a Git repository, e.g., downloaded as a ZIP.

This commit refactors the plugin to be lazy and adopts several Gradle best
practices:

- Prevents build crashes on Git failures by gracefully catching non-zero
  exit codes, e.g., when checked out in a detached HEAD state.
- Moves the branch validation out of the task's main execution action
  and into an `onlyIf` predicate, allowing Gradle to skip the task
  entirely instead of executing an early return. This makes the skip
  outcome and reason visible in a Build Scan, rather than making it
  appear as if it executed.
- Defers the Git `exec` call to the execution phase using a lazy provider.
- Makes the task configuration cache compatible by avoiding illegal
  `Project` access inside the execution-time `onlyIf` closure.
- Improves user-facing logs and adds actionable bypass instructions when
  the project version doesn't match the branch version.

Signed-off-by: Eric Haag <ehaag@gradle.com>
2026-03-23 14:49:58 -04:00
Andrey Litvitski
2fda37de53 Fix equals nullability annotations for jspecify compliance
In this commit, we added `@Nullable` to equals methods of classes that
support `jspecify` for consistency with other Spring projects and to
avoid bugs that caused other Spring projects to do this natively.

Closes: gh-18929, gh-18927

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-03-23 09:25:57 -06:00
Andreas Asplund
330c565178 Implement equals and hashCode closes gh-18882
Signed-off-by: Andreas Asplund <andreas@asplund.biz>
2026-03-23 08:06:31 -06:00
Joe Grandja
1db0d4f83d Enable null-safety in spring-security-oauth2-authorization-server
Closes gh-18937
2026-03-23 05:07:14 -04:00
Joe Grandja
fe24bd3d0c Remove checkstyle suppressions for spring-security-oauth2-authorization-server
Issue gh-18937
2026-03-23 05:06:59 -04:00
dependabot[bot]
e6df831943 Bump com.fasterxml.jackson:jackson-bom from 2.21.1 to 2.21.2
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.21.1 to 2.21.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.21.1...jackson-bom-2.21.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-version: 2.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 03:19:04 +00:00
Josh Cummings
f35b4aa518 Merge branch '7.0.x' 2026-03-20 21:28:22 -06:00
Josh Cummings
4542f58be7 Merge branch '6.5.x' into 7.0.x 2026-03-20 21:27:04 -06:00
Tran Ngoc Nhan
62f33d3fcf Add equals and hashCode to HttpMethodRequestMatcher
Closes gh-18911

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-20 21:22:20 -06:00
Bae Jihong
e9f331c30c Add test code for setAuthorizationManagerFactory
- add test for setAuthorizationManagerFactory that is a alternative to setTrustResolver and setDefaultRolePrefix

Closes gh-18412

Signed-off-by: Bae Jihong <dasog@naver.com>
2026-03-20 20:16:54 -06:00
Bae Jihong
bc4cc434fe Refactor code to remove compiler warnings
- replace setTrustResolver with setAuthorizationManagerFactory in MethodSecurityExpressionRootTests
- resolve raw type warning in ExpressionBasedMessageSecurityMetadataSourceFactoryTests

Closes gh-18412

Signed-off-by: Bae Jihong <dasog@naver.com>
2026-03-20 20:16:54 -06:00
Bae Jihong
5a694869fa Add @SupressWarnings(deprecation) for existing functions
- add @SupressWarnings(deprecation) because of deprecated part in logic

Closes gh-18412

Signed-off-by: Bae Jihong <dasog@naver.com>
2026-03-20 20:16:54 -06:00
Bae Jihong
ee06badcb6 Add @SuppressWarnings(unchecked, rawtypes) on functions in deprecated class
Closes gh-18412

Signed-off-by: Bae Jihong <dasog@naver.com>
2026-03-20 20:16:54 -06:00
Bae Jihong
9b108df1dc Add @SuppressWarnings(deprecation) on tests
- add on tests for deprecated class
- add on tests for specific deprecated function

Closes gh-18412

Signed-off-by: Bae Jihong <dasog@naver.com>
2026-03-20 20:16:54 -06:00
Josh Cummings
d76fb7f2e6 Polish WebAttributes ApplicationContext Support
Closes gh-8843

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 16:44:40 -06:00
wonderfulrosemari
846794d31b Prefer dispatcher context for authorize tag beans
Signed-off-by: wonderfulrosemari <whwlsgur1419@naver.com>
2026-03-20 16:44:40 -06:00
Josh Cummings
c000477c37 Polish Clarify @WithSecurityContext thread scope 2026-03-20 16:43:21 -06:00
wonderfulrosemari
2a013ffaa2 Clarify @WithSecurityContext thread scope
Signed-off-by: wonderfulrosemari <whwlsgur1419@naver.com>
2026-03-20 16:43:21 -06:00
Josh Cummings
ea05089443 Polish Formatting
Closes gh-18381

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 15:38:27 -06:00
Giacomo Baso
7b282c3a17 Relax client_id validation in AtJwtBuilder
RFC 9068 requires that access token JWTs include the `client_id`
claim, but it does not require resource servers to validate it against
a specific value.

Relates to gh-18381

Signed-off-by: Giacomo Baso <gbaso@users.noreply.github.com>
2026-03-20 15:38:27 -06:00
Josh Cummings
78015d251c Merge branch '7.0.x' 2026-03-20 15:28:44 -06:00
Josh Cummings
956561e143 Merge branch '6.5.x' into 7.0.x 2026-03-20 15:28:36 -06:00
Rob Winch
9fed1ac8c3 New line per sentence
Signed-off-by: Rob Winch <362503+rwinch@users.noreply.github.com>
2026-03-20 15:28:21 -06:00
Josh Cummings
9dbe3bdcc0 Polish Session Management Persistence Docs
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 15:28:21 -06:00
sankranti
d547ae0181 Fix defaults description in Session Management doc
Corrected that starting from Spring Security 6
security context is not automatically saved by default.

Signed-off-by: sankranti <sankranty@gmail.com>
2026-03-20 15:28:21 -06:00
Josh Cummings
e88cb81a7a Merge branch '7.0.x' 2026-03-20 15:22:56 -06:00
dependabot[bot]
b8b1278e1f Bump @springio/antora-extensions from 1.14.7 to 1.14.9 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.7 to 1.14.9.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.7...v1.14.9)

---
updated-dependencies:
- dependency-name: "@springio/antora-extensions"
  dependency-version: 1.14.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 15:22:06 -06:00
dependabot[bot]
381047e386 Bump spring-io/spring-security-release-tools from 1.0.14 to 1.0.15
Bumps [spring-io/spring-security-release-tools](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 15:21:53 -06:00
Josh Cummings
fbbbd46bee Update asciidoctor-extensions to 1.0.0-alpha.18
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 21:21:22 +00:00
Josh Cummings
fe0d7de41b Update LDAP Nullability Checkstyle Rules
Issue gh-17818

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 15:21:02 -06:00
Josh Cummings
c2fd0f23fe Merge branch '7.0.x' 2026-03-20 15:00:15 -06:00
Josh Cummings
8abffbd0df Merge branch '6.5.x' into 7.0.x 2026-03-20 15:00:02 -06:00
dependabot[bot]
376b40a735 Bump io.spring.gradle:spring-security-release-plugin
Bumps [io.spring.gradle:spring-security-release-plugin](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.14...v1.0.15)

---
updated-dependencies:
- dependency-name: io.spring.gradle:spring-security-release-plugin
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:58:20 -06:00
dependabot[bot]
89fa1cbdd2 Bump spring-io/spring-security-release-tools/.github/workflows/build.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/build.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/build.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:57:09 -06:00
dependabot[bot]
0d75e6d10c Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.17 to 1.0.0-alpha.18.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.17...v1.0.0-alpha.18)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-version: 1.0.0-alpha.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:56:46 -06:00
dependabot[bot]
01758c4c59 Bump spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:56:10 -06:00
dependabot[bot]
f37833a59c Bump spring-io/spring-security-release-tools/.github/workflows/test.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/test.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/test.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:55:52 -06:00
dependabot[bot]
52e6c4c4be Bump spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:55:38 -06:00
dependabot[bot]
874dce4407 Bump @springio/antora-extensions from 1.14.7 to 1.14.9 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.7 to 1.14.9.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.7...v1.14.9)

---
updated-dependencies:
- dependency-name: "@springio/antora-extensions"
  dependency-version: 1.14.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:54:26 -06:00
dependabot[bot]
f21e8af830 Bump spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:54:11 -06:00
dependabot[bot]
4354e47b0a Bump gradle-wrapper from 9.4.0 to 9.4.1
Bumps gradle-wrapper from 9.4.0 to 9.4.1.

---
updated-dependencies:
- dependency-name: gradle-wrapper
  dependency-version: 9.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 03:19:24 +00:00
dependabot[bot]
399ef5b663 Bump spring-io/spring-security-release-tools from 1.0.14 to 1.0.15
Bumps [spring-io/spring-security-release-tools](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.14...b92832ecbc7cbe969201e6beafbde0ee400cf095)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 00:58:59 +00:00
dependabot[bot]
1f39525052 Bump @springio/antora-extensions from 1.14.7 to 1.14.9 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.7 to 1.14.9.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.7...v1.14.9)

---
updated-dependencies:
- dependency-name: "@springio/antora-extensions"
  dependency-version: 1.14.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 00:47:14 +00:00
dependabot[bot]
7a0a29b800 Bump spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 00:46:34 +00:00
dependabot[bot]
9dfbd681ab Bump spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/perform-release.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.14...b92832ecbc7cbe969201e6beafbde0ee400cf095)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 00:45:50 +00:00
dependabot[bot]
4e53ebb75b Bump spring-io/spring-security-release-tools/.github/workflows/test.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/test.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](729fed56d4...b92832ecbc)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/test.yml
  dependency-version: 1.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 00:45:47 +00:00
dependabot[bot]
7eed4641da Bump flatted from 3.3.1 to 3.4.2 in /javascript
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.1 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.1...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 00:33:02 +00:00
Joe Grandja
09ce639c4b Enable null-safety in spring-security-oauth2-resource-server
Closes gh-17822
2026-03-19 06:21:08 -04:00
Joe Grandja
1cb9db4f2d Remove checkstyle suppressions for spring-security-oauth2-resource-server
Issue gh-17822
2026-03-19 04:46:34 -04:00
Gasper Kojek
14d469cec1 Exclude target directories from checkstyleNohttp source inputs
The kerberos-client/target and kerberos-test/target directories contain
.keytab files generated during test execution. These directories only
exist after the first build, causing a cache miss for checkstyleNohttp
in subsequent builds since the source input set changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Gasper Kojek <gkojek@gradle.com>
2026-03-18 10:40:52 -04:00
Gasper Kojek
49bea1dd15 Exclude build output directories from nohttp source set
The checkstyleNohttp task scans the entire project tree for non-HTTPS
URLs. Without excluding **/build/**, subproject build output directories
generated during the first build become additional source inputs for
subsequent builds, changing the cache key and causing cache misses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Gasper Kojek <gkojek@gradle.com>
2026-03-18 10:40:52 -04:00
Joe Grandja
baad23caab Enable null-safety in spring-security-oauth2-client
Closes gh-17819
2026-03-18 05:04:30 -04:00
Joe Grandja
bb062585a8 Remove checkstyle suppressions for spring-security-oauth2-client
Issue gh-17819
2026-03-18 04:04:12 -04:00
Robert Winch
ea2f2302da
Add MultiFactorCondition.WEBAUTHN_REGISTERED
Closes gh-18923
2026-03-17 17:20:58 -05:00
Robert Winch
bd7171140e
Support Customizer<AdditionalRequiredFactorsBuilder<Object>>>
Closes gh-18922
2026-03-17 17:20:58 -05:00
Robert Winch
c71b178f63
Remove Unnecessary ObjectProvider<RoleHierarchy> roleHierarchy parameter
Closes gh-18921
2026-03-17 17:20:58 -05:00
Robert Winch
28acf62936
AuthorizationManagerFactories.when
Closes gh-18920
2026-03-17 17:20:58 -05:00
Robert Winch
8224b16caf
Add ConditionalAuthorizationManager
Closes gh-18919
2026-03-17 17:20:57 -05:00
dependabot[bot]
5a827d86d5 Bump org-jetbrains-kotlin from 2.3.10 to 2.3.20
Bumps `org-jetbrains-kotlin` from 2.3.10 to 2.3.20.

Updates `org.jetbrains.kotlin:kotlin-bom` from 2.3.10 to 2.3.20
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/v2.3.20/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.10...v2.3.20)

Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.3.10 to 2.3.20
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/v2.3.20/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.10...v2.3.20)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin:kotlin-bom
  dependency-version: 2.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin
  dependency-version: 2.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 03:19:49 +00:00
dependabot[bot]
67c5b4f5a2 Bump org.hibernate.orm:hibernate-core from 7.2.7.Final to 7.3.0.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 7.2.7.Final to 7.3.0.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/7.3.0/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/7.2.7...7.3.0)

---
updated-dependencies:
- dependency-name: org.hibernate.orm:hibernate-core
  dependency-version: 7.3.0.Final
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 03:19:09 +00:00
dependabot[bot]
8f30567b83 Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.17 to 1.0.0-alpha.18.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.17...v1.0.0-alpha.18)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-version: 1.0.0-alpha.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 23:33:53 +00:00
github-actions[bot]
e044c24952 Next development version 2026-03-16 19:07:08 +00:00
github-actions[bot]
8aae3490da Next development version 2026-03-16 19:05:05 +00:00
github-actions[bot]
9bd793ffe6 Release 7.0.4 2026-03-16 18:16:34 +00:00
github-actions[bot]
96ceb535f4 Next development version 2026-03-16 18:13:58 +00:00
github-actions[bot]
0c54a55ae8 Release 6.5.9 2026-03-16 17:40:54 +00:00
573 changed files with 7123 additions and 1915 deletions

View File

@ -5,6 +5,7 @@ on:
branches:
- main
- '*.x'
- 'docs-build'
run-name: Merge Dependabot PR ${{ github.ref_name }}

View File

@ -14,7 +14,7 @@ permissions:
jobs:
snapshot-test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
strategy:
matrix:
include:
@ -31,6 +31,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}

View File

@ -16,7 +16,7 @@ permissions:
jobs:
perform-release:
name: Perform Release
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-perform-release: true
project-version: ${{ inputs.version }}

View File

@ -30,6 +30,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}

View File

@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: '25'
distribution: 'temurin'
@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: '25'
distribution: 'temurin'
@ -46,6 +46,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}

View File

@ -9,7 +9,7 @@ permissions:
jobs:
update-scheduled-release-version:
name: Update Scheduled Release Version
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
secrets: inherit
send-notification:
name: Send Notification
@ -18,6 +18,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}

View File

@ -20,6 +20,8 @@ import java.io.Serial;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.annotation.SecurityAnnotationScanner;
import org.springframework.util.Assert;
@ -50,7 +52,7 @@ public class SecurityConfig implements ConfigAttribute {
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (obj instanceof ConfigAttribute attr) {
return this.attrib.equals(attr.getAttribute());
}

View File

@ -114,8 +114,10 @@ public final class DelegatingMethodSecurityMetadataSource extends AbstractMethod
}
@Override
public boolean equals(Object other) {
DefaultCacheKey otherKey = (DefaultCacheKey) other;
public boolean equals(@Nullable Object other) {
if (!(other instanceof DefaultCacheKey otherKey)) {
return false;
}
return (this.method.equals(otherKey.method)
&& ObjectUtils.nullSafeEquals(this.targetClass, otherKey.targetClass));
}

View File

@ -265,7 +265,7 @@ public class MapBasedMethodSecurityMetadataSource extends AbstractFallbackMethod
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}

View File

@ -145,6 +145,7 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor
.map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r));
}
@SuppressWarnings("unchecked")
private static <T extends Publisher<?>> @Nullable T proceed(final MethodInvocation invocation) {
try {
return (T) invocation.proceed();

View File

@ -111,6 +111,7 @@ public class AclEntryAfterInvocationCollectionFilteringProvider extends Abstract
return returnedObject;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Filterer getFilterer(Object returnedObject) {
if (returnedObject instanceof Collection) {
return new CollectionFilterer((Collection) returnedObject);

View File

@ -50,6 +50,7 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
* @param authorizeExpression the {@link Expression} to use. Cannot be null
* @param matcher the {@link MessageMatcher} used to match the messages.
*/
@SuppressWarnings("unchecked")
MessageExpressionConfigAttribute(Expression authorizeExpression, MessageMatcher<?> matcher) {
Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
Assert.notNull(matcher, "matcher cannot be null");

View File

@ -41,6 +41,7 @@ public class DefaultWebSecurityExpressionHandler extends AbstractSecurityExpress
private String defaultRolePrefix = DEFAULT_ROLE_PREFIX;
@Override
@SuppressWarnings("deprecation")
protected SecurityExpressionOperations createSecurityExpressionRoot(@Nullable Authentication authentication,
FilterInvocation fi) {
FilterInvocationExpressionRoot root = new FilterInvocationExpressionRoot(() -> authentication, fi);

View File

@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class AuthenticationCredentialsNotFoundEventTests {
@Test

View File

@ -32,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class AuthorizationFailureEventTests {
private final UsernamePasswordAuthenticationToken foo = UsernamePasswordAuthenticationToken.unauthenticated("foo",

View File

@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class AuthorizedEventTests {
@Test

View File

@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class SecurityConfigTests {
@Test

View File

@ -32,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Luke Taylor
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class Jsr250MethodSecurityMetadataSourceTests {
Jsr250MethodSecurityMetadataSource mds;

View File

@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class Jsr250VoterTests {
// SEC-1443

View File

@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.fail;
* @author Ben Alex
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class SecuredAnnotationSecurityMetadataSourceTests {
private SecuredAnnotationSecurityMetadataSource mds = new SecuredAnnotationSecurityMetadataSource();

View File

@ -79,11 +79,13 @@ public class DefaultMethodSecurityExpressionHandlerTests {
}
@Test
@SuppressWarnings("deprecation")
public void setTrustResolverNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setTrustResolver(null));
}
@Test
@SuppressWarnings("deprecation")
public void createEvaluationContextCustomTrustResolver() {
setupMocks();
this.handler.setTrustResolver(this.trustResolver);
@ -175,7 +177,7 @@ public class DefaultMethodSecurityExpressionHandlerTests {
@Test
public void createEvaluationContextSupplierAuthentication() {
setupMocks();
Supplier<Authentication> mockAuthenticationSupplier = mock(Supplier.class);
Supplier<Authentication> mockAuthenticationSupplier = mock();
given(mockAuthenticationSupplier.get()).willReturn(this.authentication);
EvaluationContext context = this.handler.createEvaluationContext(mockAuthenticationSupplier,
this.methodInvocation);

View File

@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @since 5.2
*/
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class ExpressionBasedPreInvocationAdviceTests {
@Mock

View File

@ -34,7 +34,7 @@ import org.springframework.security.util.SimpleMethodInvocation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "deprecation" })
public class MethodExpressionVoterTests {
private TestingAuthenticationToken joe = new TestingAuthenticationToken("joe", "joespass", "ROLE_blah");

View File

@ -27,6 +27,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.core.Authentication;
import static org.mockito.ArgumentMatchers.any;
@ -58,7 +59,9 @@ public class MethodSecurityExpressionRootTests {
this.ctx = new StandardEvaluationContext();
this.ctx.setRootObject(this.root);
this.trustResolver = mock(AuthenticationTrustResolver.class);
this.root.setTrustResolver(this.trustResolver);
DefaultAuthorizationManagerFactory<MethodInvocation> authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>();
authorizationManagerFactory.setTrustResolver(this.trustResolver);
this.root.setAuthorizationManagerFactory(authorizationManagerFactory);
}
@Test

View File

@ -44,6 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Luke Taylor
* @since 3.0
*/
@SuppressWarnings("deprecation")
public class PrePostAnnotationSecurityMetadataSourceTests {
private PrePostAnnotationSecurityMetadataSource mds = new PrePostAnnotationSecurityMetadataSource(

View File

@ -32,6 +32,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class AbstractSecurityInterceptorTests {
@Test

View File

@ -38,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "deprecation" })
public class AfterInvocationProviderManagerTests {
@Test

View File

@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class InterceptorStatusTokenTests {
@Test

View File

@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class NullRunAsManagerTests {
@Test

View File

@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
/**
* Tests {@link RunAsImplAuthenticationProvider}.
*/
@SuppressWarnings("deprecation")
public class RunAsImplAuthenticationProviderTests {
@Test

View File

@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.fail;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class RunAsManagerImplTests {
@Test

View File

@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class RunAsUserTokenTests {
@Test

View File

@ -65,7 +65,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
* @author Ben Alex
* @author Rob Winch
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "deprecation" })
public class MethodSecurityInterceptorTests {
private TestingAuthenticationToken token;

View File

@ -33,6 +33,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class MethodSecurityMetadataSourceAdvisorTests {
@Test

View File

@ -62,6 +62,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
* @author Luke Taylor
* @author Rob Winch
*/
@SuppressWarnings("deprecation")
public class AspectJMethodSecurityInterceptorTests {
private TestingAuthenticationToken token;

View File

@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Luke Taylor
* @since 2.0.4
*/
@SuppressWarnings("deprecation")
public class MapBasedMethodSecurityMetadataSourceTests {
private final List<ConfigAttribute> ROLE_A = SecurityConfig.createList("ROLE_A");

View File

@ -49,6 +49,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class MethodInvocationPrivilegeEvaluatorTests {
private TestingAuthenticationToken token;

View File

@ -36,7 +36,7 @@ import static org.mockito.Mockito.mock;
/**
* @author Luke Taylor
*/
@SuppressWarnings({ "unchecked" })
@SuppressWarnings({ "unchecked", "deprecation" })
public class DelegatingMethodSecurityMetadataSourceTests {
DelegatingMethodSecurityMetadataSource mds;

View File

@ -29,6 +29,7 @@ import org.springframework.security.access.intercept.aspectj.MethodInvocationAda
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class PostInvocationAdviceProviderTests {
@Mock

View File

@ -29,6 +29,7 @@ import org.springframework.security.access.intercept.aspectj.MethodInvocationAda
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class PreInvocationAuthorizationAdviceVoterTests {
@Mock

View File

@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "deprecation" })
public class AbstractAccessDecisionManagerTests {
@Test

View File

@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class AbstractAclVoterTests {
private AbstractAclVoter voter = new AbstractAclVoter() {

View File

@ -40,6 +40,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class AffirmativeBasedTests {
private final List<ConfigAttribute> attrs = new ArrayList<>();

View File

@ -37,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class AuthenticatedVoterTests {
private Authentication createAnonymous() {

View File

@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class ConsensusBasedTests {
@Test

View File

@ -32,6 +32,7 @@ import org.springframework.security.core.Authentication;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class DenyAgainVoter implements AccessDecisionVoter<Object> {
@Override

View File

@ -34,6 +34,7 @@ import org.springframework.security.core.Authentication;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class DenyVoter implements AccessDecisionVoter<Object> {
@Override

View File

@ -25,6 +25,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("deprecation")
public class RoleHierarchyVoterTests {
@Test

View File

@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class RoleVoterTests {
@Test

View File

@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class UnanimousBasedTests {
private UnanimousBased makeDecisionManager() {

View File

@ -44,7 +44,7 @@ import static org.mockito.Mockito.verify;
/**
* @author Luke Taylor
*/
@SuppressWarnings({ "unchecked" })
@SuppressWarnings({ "unchecked", "deprecation" })
public class AclEntryAfterInvocationCollectionFilteringProviderTests {
@Test

View File

@ -48,7 +48,7 @@ import static org.mockito.Mockito.verify;
/**
* @author Luke Taylor
*/
@SuppressWarnings({ "unchecked" })
@SuppressWarnings({ "unchecked", "deprecation" })
public class AclEntryAfterInvocationProviderTests {
@Test

View File

@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class ExpressionBasedMessageSecurityMetadataSourceFactoryTests {
@Mock
@ -57,7 +58,7 @@ public class ExpressionBasedMessageSecurityMetadataSourceFactoryTests {
MessageSecurityMetadataSource source;
MessageSecurityExpressionRoot rootObject;
MessageSecurityExpressionRoot<Object> rootObject;
@BeforeEach
public void setup() {
@ -68,7 +69,7 @@ public class ExpressionBasedMessageSecurityMetadataSourceFactoryTests {
this.matcherToExpression.put(this.matcher2, this.expression2);
this.source = ExpressionBasedMessageSecurityMetadataSourceFactory
.createExpressionMessageMetadataSource(this.matcherToExpression);
this.rootObject = new MessageSecurityExpressionRoot(this.authentication, this.message);
this.rootObject = new MessageSecurityExpressionRoot<>(this.authentication, this.message);
}
@Test

View File

@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class MessageExpressionConfigAttributeTests {
@Mock

View File

@ -44,6 +44,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class MessageExpressionVoterTests {
@Mock
@ -76,6 +77,7 @@ public class MessageExpressionVoterTests {
}
@Test
@SuppressWarnings("unchecked")
public void voteGranted() {
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(true);
given(this.matcher.matcher(any())).willCallRealMethod();
@ -84,6 +86,7 @@ public class MessageExpressionVoterTests {
}
@Test
@SuppressWarnings("unchecked")
public void voteDenied() {
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(false);
given(this.matcher.matcher(any())).willCallRealMethod();
@ -92,6 +95,7 @@ public class MessageExpressionVoterTests {
}
@Test
@SuppressWarnings("unchecked")
public void voteAbstain() {
this.attributes = Arrays.<ConfigAttribute>asList(new SecurityConfig("ROLE_USER"));
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
@ -99,11 +103,13 @@ public class MessageExpressionVoterTests {
}
@Test
@SuppressWarnings("unchecked")
public void supportsObjectClassFalse() {
assertThat(this.voter.supports(Object.class)).isFalse();
}
@Test
@SuppressWarnings("unchecked")
public void supportsMessageClassTrue() {
assertThat(this.voter.supports(Message.class)).isTrue();
}
@ -119,11 +125,13 @@ public class MessageExpressionVoterTests {
}
@Test
@SuppressWarnings("unchecked")
public void setExpressionHandlerNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.voter.setExpressionHandler(null));
}
@Test
@SuppressWarnings("unchecked")
public void customExpressionHandler() {
this.voter.setExpressionHandler(this.expressionHandler);
given(this.expressionHandler.createEvaluationContext(this.authentication, this.message))
@ -136,6 +144,7 @@ public class MessageExpressionVoterTests {
}
@Test
@SuppressWarnings("unchecked")
public void postProcessEvaluationContext() {
final MessageExpressionConfigAttribute configAttribute = mock(MessageExpressionConfigAttribute.class);
this.voter.setExpressionHandler(this.expressionHandler);

View File

@ -47,6 +47,7 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class ChannelSecurityInterceptorTests {
@Mock

View File

@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("deprecation")
public class DefaultMessageSecurityMetadataSourceTests {
@Mock

View File

@ -43,6 +43,7 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class DefaultWebInvocationPrivilegeEvaluatorTests {
private AccessDecisionManager adm;

View File

@ -42,7 +42,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "deprecation" })
public class ChannelDecisionManagerImplTests {
@Test

View File

@ -39,6 +39,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class ChannelProcessingFilterTests {
@Test

View File

@ -35,6 +35,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class InsecureChannelProcessorTests {
@Test

View File

@ -36,6 +36,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class RetryWithHttpEntryPointTests {
@Test

View File

@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class RetryWithHttpsEntryPointTests {
@Test

View File

@ -35,6 +35,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class SecureChannelProcessorTests {
@Test

View File

@ -64,6 +64,7 @@ public class DefaultWebSecurityExpressionHandlerTests {
}
@Test
@SuppressWarnings("deprecation")
public void expressionPropertiesAreResolvedAgainstAppContextBeans() {
StaticApplicationContext appContext = new StaticApplicationContext();
RootBeanDefinition bean = new RootBeanDefinition(SecurityConfig.class);
@ -78,11 +79,13 @@ public class DefaultWebSecurityExpressionHandlerTests {
}
@Test
@SuppressWarnings("deprecation")
public void setTrustResolverNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setTrustResolver(null));
}
@Test
@SuppressWarnings("deprecation")
public void createEvaluationContextCustomTrustResolver() {
this.handler.setTrustResolver(this.trustResolver);
Expression expression = this.handler.getExpressionParser().parseExpression("anonymous");

View File

@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
/**
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class ExpressionBasedFilterInvocationSecurityMetadataSourceTests {
@Test

View File

@ -41,7 +41,7 @@ import static org.mockito.Mockito.mock;
/**
* @author Luke Taylor
*/
@SuppressWarnings({ "unchecked" })
@SuppressWarnings({ "unchecked", "deprecation" })
public class WebExpressionVoterTests {
private Authentication user = new TestingAuthenticationToken("user", "pass", "X");

View File

@ -40,6 +40,7 @@ import static org.mockito.Mockito.mock;
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class DefaultFilterInvocationSecurityMetadataSourceTests {
private DefaultFilterInvocationSecurityMetadataSource fids;

View File

@ -62,6 +62,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
* @author Luke Taylor
* @author Rob Winch
*/
@SuppressWarnings("deprecation")
public class FilterSecurityInterceptorTests {
private AuthenticationManager am;

View File

@ -16,6 +16,8 @@
package org.springframework.security.acls.domain;
import org.jspecify.annotations.Nullable;
import org.springframework.security.acls.model.Permission;
/**
@ -52,7 +54,7 @@ public abstract class AbstractPermission implements Permission {
}
@Override
public final boolean equals(Object obj) {
public final boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}

View File

@ -63,7 +63,7 @@ public class AccessControlEntryImpl implements AccessControlEntry, AuditableAcce
}
@Override
public boolean equals(Object arg0) {
public boolean equals(@Nullable Object arg0) {
if (!(arg0 instanceof AccessControlEntryImpl)) {
return false;
}

View File

@ -278,7 +278,7 @@ public class AclImpl implements Acl, MutableAcl, AuditableAcl, OwnershipAcl {
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (obj == this) {
return true;
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.acls.domain;
import org.jspecify.annotations.Nullable;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
@ -47,7 +49,7 @@ public class GrantedAuthoritySid implements Sid {
}
@Override
public boolean equals(Object object) {
public boolean equals(@Nullable Object object) {
if ((object == null) || !(object instanceof GrantedAuthoritySid)) {
return false;
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.acls.domain;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.jspecify.annotations.Nullable;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -97,7 +99,7 @@ public class ObjectIdentityImpl implements ObjectIdentity {
* @return <code>true</code> if the presented object matches this object
*/
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (obj == null || !(obj instanceof ObjectIdentityImpl)) {
return false;
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.acls.domain;
import org.jspecify.annotations.Nullable;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@ -46,7 +48,7 @@ public class PrincipalSid implements Sid {
}
@Override
public boolean equals(Object object) {
public boolean equals(@Nullable Object object) {
if ((object == null) || !(object instanceof PrincipalSid)) {
return false;
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.acls.model;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
/**
* Represents the identity of an individual domain object instance.
*
@ -40,7 +42,7 @@ public interface ObjectIdentity extends Serializable {
* @see Object#equals(Object)
*/
@Override
boolean equals(Object obj);
boolean equals(@Nullable Object obj);
/**
* Obtains the actual identifier. This identifier must not be reused to represent

View File

@ -18,6 +18,8 @@ package org.springframework.security.acls.model;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
/**
* A security identity recognised by the ACL system.
*
@ -40,7 +42,7 @@ public interface Sid extends Serializable {
* @return <code>true</code> if the objects are equal, <code>false</code> otherwise
*/
@Override
boolean equals(Object obj);
boolean equals(@Nullable Object obj);
/**
* Refer to the <code>java.lang.Object</code> documentation for the interface

View File

@ -80,7 +80,7 @@ develocity {
}
nohttp {
source.exclude "buildSrc/build/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
source.exclude "buildSrc/build/**", "**/build/**", "**/target/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
}

View File

@ -3,6 +3,7 @@ plugins {
id "groovy-gradle-plugin"
id "java"
id "groovy"
id "org.jetbrains.dokka" version "2.2.0"
}
java {
@ -79,6 +80,7 @@ dependencies {
implementation libs.io.spring.javaformat.spring.javaformat.gradle.plugin
implementation libs.io.spring.nohttp.nohttp.gradle
implementation libs.org.jetbrains.kotlin.kotlin.gradle.plugin
implementation libs.org.jetbrains.dokka.dokka.gradle.plugin
implementation (libs.net.sourceforge.htmlunit) {
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
}

View File

@ -5,6 +5,7 @@ import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.PluginManager
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.bundling.Zip
/**
@ -18,6 +19,37 @@ public class DocsPlugin implements Plugin<Project> {
PluginManager pluginManager = project.getPluginManager();
pluginManager.apply(BasePlugin);
pluginManager.apply(JavadocApiPlugin);
pluginManager.apply("org.jetbrains.dokka");
project.rootProject.subprojects { subproject ->
subproject.pluginManager.withPlugin("security-kotlin") {
subproject.pluginManager.apply("org.jetbrains.dokka")
configureDokka(subproject)
project.dependencies.add("dokka", subproject)
}
}
project.extensions.configure("dokka") { dokka ->
dokka.moduleName.set(Utils.getProjectName(project) + " Kotlin API")
}
project.tasks.named("dokkaGeneratePublicationHtml").configure { it.dependsOn("api") }
project.tasks.register("syncAntoraAttachments", Sync) { sync ->
sync.group = 'Documentation'
sync.description = 'Syncs the Antora attachments'
sync.into(project.layout.buildDirectory.dir('generated-antora-resources/modules/ROOT/assets/attachments/api'))
sync.from(project.provider({ project.tasks.api.outputs })) { copy ->
copy.into('java')
}
sync.from(project.tasks.named("dokkaGeneratePublicationHtml")) { copy ->
copy.into('kotlin')
}
}
project.tasks.register("generateAntoraResources") {
it.dependsOn 'generateAntoraYml', 'syncAntoraAttachments'
}
Task docsZip = project.tasks.create('docsZip', Zip) {
dependsOn 'api'
@ -41,4 +73,16 @@ public class DocsPlugin implements Plugin<Project> {
}
project.tasks.assemble.dependsOn docs
}
void configureDokka(Project project) {
project.extensions.configure("dokka") { dokka ->
dokka.dokkaSourceSets.configureEach { spec ->
spec.suppressedFiles.from(project.fileTree("src/main/java"))
spec.externalDocumentationLinks.register("javadoc") {
it.url.set(new URI("https://docs.spring.io/${Utils.getProjectName(project)}/reference/${project.rootProject.version}/api/java/"))
it.packageListUrl.set(project.layout.buildDirectory.file("api/element-list").get().asFile.toURI())
}
}
}
}
}

View File

@ -21,8 +21,12 @@ import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.Logger;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
@ -30,6 +34,7 @@ import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.VerificationException;
import org.gradle.process.ExecOutput;
import java.io.IOException;
import java.nio.file.Files;
@ -44,21 +49,53 @@ public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
TaskProvider<CheckExpectedBranchVersionTask> checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> {
task.setGroup("Build");
task.setDescription("Check if the project version matches the branch version");
task.onlyIf("skipCheckExpectedBranchVersion property is false or not present", CheckExpectedBranchVersionPlugin::skipPropertyFalseOrNotPresent);
task.onlyIf("Property 'skipCheckExpectedBranchVersion' is false or not present", skipPropertyFalseOrNotPresent(project.getProviders()));
task.onlyIf("Branch name matches expected version pattern *.x", CheckExpectedBranchVersionPlugin::isVersionBranch);
task.getVersion().convention(project.provider(() -> project.getVersion().toString()));
task.getBranchName().convention(project.getProviders().exec((execSpec) -> execSpec.setCommandLine("git", "symbolic-ref", "--short", "HEAD")).getStandardOutput().getAsText());
task.getBranchName().convention(getBranchName(project.getProviders(), project.getLogger()));
task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("check-expected-branch-version"));
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask));
}
private static boolean skipPropertyFalseOrNotPresent(Task task) {
return task.getProject()
.getProviders()
private static Spec<Task> skipPropertyFalseOrNotPresent(ProviderFactory providers) {
Provider<Boolean> skipPropertyFalseOrNotPresent = providers
.gradleProperty("skipCheckExpectedBranchVersion")
.orElse("false")
.map("false"::equalsIgnoreCase)
.get();
.map("false"::equalsIgnoreCase);
return (task) -> skipPropertyFalseOrNotPresent.get();
}
private static boolean isVersionBranch(Task task) {
return isVersionBranch((CheckExpectedBranchVersionTask) task);
}
private static boolean isVersionBranch(CheckExpectedBranchVersionTask task) {
String branchName = task.getBranchName().getOrNull();
if (branchName == null) {
return false;
}
return branchName.matches("^[0-9]+\\.[0-9]+\\.x$");
}
private static Provider<String> getBranchName(ProviderFactory providers, Logger logger) {
ExecOutput execOutput = providers.exec((execSpec) -> {
execSpec.setCommandLine("git", "symbolic-ref", "--short", "HEAD");
execSpec.setIgnoreExitValue(true);
});
return providers.provider(() -> {
int exitValue = execOutput.getResult().get().getExitValue();
if (exitValue != 0) {
logger.warn("Unable to determine branch name. Received exit code '{}' from `git`.", exitValue);
logger.warn(execOutput.getStandardError().getAsText().getOrNull());
return null;
}
String branchName = execOutput.getStandardOutput().getAsText().get().trim();
logger.info("Git branch name is '{}'.", branchName);
return branchName;
});
}
@CacheableTask
@ -77,15 +114,10 @@ public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
public void run() {
String version = getVersion().get();
String branchVersion = getBranchName().map(String::trim).get();
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
String msg = String.format("Branch version [%s] does not match *.x, ignoring", branchVersion);
getLogger().warn(msg);
writeExpectedVersionOutput(msg);
return;
}
if (!versionsMatch(version, branchVersion)) {
String msg = String.format("Project version [%s] does not match branch version [%s]. " +
"Please verify that the branch contains the right version.", version, branchVersion);
"Please verify that the branch contains the right version. " +
"To bypass this check, run the build with -PskipCheckExpectedBranchVersion.", version, branchVersion);
writeExpectedVersionOutput(msg);
throw new VerificationException(msg);
}

View File

@ -124,7 +124,7 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
}
@Override
public boolean equals(final Object obj) {
public boolean equals(@Nullable final Object obj) {
if (!super.equals(obj)) {
return false;
}

View File

@ -71,7 +71,7 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}

View File

@ -16,15 +16,16 @@
package org.springframework.security.config.annotation.authorization;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.authorization.AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.config.Customizer;
/**
* Uses {@link EnableMultiFactorAuthentication} to configure a
@ -39,10 +40,13 @@ class AuthorizationManagerFactoryConfiguration implements ImportAware {
private String[] authorities;
@Bean
DefaultAuthorizationManagerFactory authorizationManagerFactory(ObjectProvider<RoleHierarchy> roleHierarchy) {
AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object> builder = AuthorizationManagerFactories
.multiFactor()
DefaultAuthorizationManagerFactory authorizationManagerFactory(
List<Customizer<AdditionalRequiredFactorsBuilder<Object>>> additionalRequiredFactorsCustomizers) {
AdditionalRequiredFactorsBuilder<Object> builder = AuthorizationManagerFactories.multiFactor()
.requireFactors(this.authorities);
for (Customizer<AdditionalRequiredFactorsBuilder<Object>> customizer : additionalRequiredFactorsCustomizers) {
customizer.customize(builder);
}
return builder.build();
}

View File

@ -30,8 +30,12 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
*
* When {@link #authorities()} is specified creates a
* {@link DefaultAuthorizationManagerFactory} as a Bean with the {@link #authorities()}
* specified as additional required authorities. The configuration will be picked up by
* both
* specified as additional required authorities. When {@link #when()} is
* {@link MultiFactorCondition#WEBAUTHN_REGISTERED}, {@link #authorities()} must include
* {@link org.springframework.security.core.authority.FactorGrantedAuthority#WEBAUTHN_AUTHORITY};
* otherwise an {@link IllegalArgumentException} is thrown during configuration
* processing. When {@link #when()} is not specified (default is an empty array), no such
* requirement applies. The configuration will be picked up by both
* {@link org.springframework.security.config.annotation.web.configuration.EnableWebSecurity}
* and
* {@link org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity}.
@ -45,6 +49,19 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
* }
* </pre>
*
* <p>
* You can also publish one or more
* {@code Customizer<AdditionalRequiredFactorsBuilder<Object>>} beans to further customize
* the {@link DefaultAuthorizationManagerFactory}. For example, conditionally applying MFA
* for specific users:
*
* <pre>
* &#64;Bean
* Customizer&lt;AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder&lt;Object&gt;&gt; additionalRequiredFactorsCustomizer() {
* return (builder) -&gt; builder.when((auth) -&gt; "admin".equals(auth.getName()));
* }
* </pre>
*
* NOTE: At this time reactive applications do not support MFA and thus are not impacted.
* This will likely be enhanced in the future.
*
@ -67,4 +84,15 @@ public @interface EnableMultiFactorAuthentication {
*/
String[] authorities();
/**
* The conditions under which multi-factor authentication is required.
* <p>
* When multiple conditions are specified, they are applied as an AND (all conditions
* must be met).
* @return the conditions (default is an empty array, which requires MFA
* unconditionally)
* @since 7.1
*/
MultiFactorCondition[] when() default {};
}

View File

@ -17,16 +17,24 @@
package org.springframework.security.config.annotation.authorization;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.core.authority.FactorGrantedAuthority;
/**
* Uses {@link EnableMultiFactorAuthentication} to configure a
* {@link DefaultAuthorizationManagerFactory}.
* <p>
* When {@link EnableMultiFactorAuthentication#when()} includes
* {@link MultiFactorCondition#WEBAUTHN_REGISTERED}, validates that
* {@link EnableMultiFactorAuthentication#authorities()} includes
* {@link org.springframework.security.core.authority.FactorGrantedAuthority#WEBAUTHN_AUTHORITY}
* and throws an {@link IllegalArgumentException} if not.
*
* @author Rob Winch
* @since 7.0
@ -39,9 +47,19 @@ class MultiFactorAuthenticationSelector implements ImportSelector {
Map<String, Object> multiFactorAuthenticationAttrs = metadata
.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName());
String[] authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
List<String> imports = new ArrayList<>(2);
MultiFactorCondition[] when = (MultiFactorCondition[]) multiFactorAuthenticationAttrs.getOrDefault("when",
new MultiFactorCondition[0]);
boolean hasWebAuthn = Arrays.asList(when).contains(MultiFactorCondition.WEBAUTHN_REGISTERED);
if (hasWebAuthn && !Arrays.asList(authorities).contains(FactorGrantedAuthority.WEBAUTHN_AUTHORITY)) {
throw new IllegalArgumentException("When when() includes " + MultiFactorCondition.WEBAUTHN_REGISTERED
+ ", authorities() must include " + FactorGrantedAuthority.WEBAUTHN_AUTHORITY);
}
List<String> imports = new ArrayList<>(3);
if (authorities.length > 0) {
imports.add(AuthorizationManagerFactoryConfiguration.class.getName());
if (hasWebAuthn) {
imports.add(WhenWebAuthnRegisteredMfaConfiguration.class.getName());
}
}
imports.add(EnableMfaFiltersConfiguration.class.getName());
return imports.toArray(new String[imports.size()]);

View File

@ -0,0 +1,58 @@
/*
* Copyright 2004-present the original author 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.authorization;
/**
* Condition under which multi-factor authentication is required.
*
* @author Rob Winch
* @since 7.1
* @see EnableMultiFactorAuthentication#when()
*/
public enum MultiFactorCondition {
/**
* Require multi-factor authentication only when the user has a WebAuthn credential
* record registered.
* <p>
* When this condition is specified,
* {@link EnableMultiFactorAuthentication#authorities()} must include
* {@link org.springframework.security.core.authority.FactorGrantedAuthority#WEBAUTHN_AUTHORITY}.
* Failing to include it results in an {@link IllegalArgumentException} when the
* configuration is processed.
* <p>
* Using this condition also requires both a
* {@link org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository}
* Bean and a
* {@link org.springframework.security.web.webauthn.management.UserCredentialRepository}
* Bean to be published.
*
* <pre>
* &#64;Bean
* public PublicKeyCredentialUserEntityRepository userEntityRepository() {
* return new InMemoryPublicKeyCredentialUserEntityRepository();
* }
*
* &#64;Bean
* public UserCredentialRepository userCredentialRepository() {
* return new InMemoryUserCredentialRepository();
* }
* </pre>
*/
WEBAUTHN_REGISTERED
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2004-present the original author 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.authorization;
import java.util.function.Predicate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder;
import org.springframework.security.config.Customizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
/**
* Configuration that provides a
* {@link Customizer}&lt;{@link AdditionalRequiredFactorsBuilder}&gt; for
* {@link MultiFactorCondition#WEBAUTHN_REGISTERED}, requiring multi-factor authentication
* only when the user has a WebAuthn credential record.
*
* @author Rob Winch
* @since 7.1
* @see EnableMultiFactorAuthentication#when()
* @see MultiFactorCondition#WEBAUTHN_REGISTERED
*/
@Configuration(proxyBeanMethods = false)
class WhenWebAuthnRegisteredMfaConfiguration {
@Bean
Customizer<AdditionalRequiredFactorsBuilder<Object>> additionalRequiredFactorsCustomizer(
PublicKeyCredentialUserEntityRepository userEntityRepository,
UserCredentialRepository userCredentialRepository) {
return (builder) -> builder.withWhen((current) -> {
Predicate<Authentication> webAuthnRegisteredPredicate = new WebAuthnRegisteredPredicate(
userEntityRepository, userCredentialRepository);
if (current == null) {
return webAuthnRegisteredPredicate;
}
return current.and(webAuthnRegisteredPredicate);
});
}
private static final class WebAuthnRegisteredPredicate implements Predicate<Authentication> {
private final PublicKeyCredentialUserEntityRepository userEntityRepository;
private final UserCredentialRepository userCredentialRepository;
private WebAuthnRegisteredPredicate(PublicKeyCredentialUserEntityRepository userEntityRepository,
UserCredentialRepository userCredentialRepository) {
this.userEntityRepository = userEntityRepository;
this.userCredentialRepository = userCredentialRepository;
}
@Override
public boolean test(Authentication authentication) {
if (authentication == null || authentication.getName() == null) {
return false;
}
PublicKeyCredentialUserEntity userEntity = this.userEntityRepository
.findByUsername(authentication.getName());
if (userEntity == null) {
return false;
}
return !this.userCredentialRepository.findByUserId(userEntity.getId()).isEmpty();
}
@Override
public String toString() {
return "WEBAUTHN_REGISTERED";
}
}
}

View File

@ -27,6 +27,7 @@ import java.util.function.Supplier;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
@ -286,7 +287,7 @@ class SecurityReactorContextConfiguration {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}

View File

@ -21,15 +21,18 @@ import org.springframework.context.ApplicationContext;
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.util.Assert;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.PreFlightRequestHandler;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.PreFlightRequestFilter;
/**
* Adds {@link CorsFilter} to the Spring Security filter chain. If a bean by the name of
* corsFilter is provided, that {@link CorsFilter} is used. Else if
* corsConfigurationSource is defined, then that {@link CorsConfiguration} is used.
* Adds {@link CorsFilter} or {@link PreFlightRequestFilter} to the Spring Security filter
* chain. If a bean by the name of corsFilter is provided, that {@link CorsFilter} is
* used. Else if corsConfigurationSource is defined, then that
* {@link CorsConfigurationSource} is used. If a {@link PreFlightRequestHandler} is set on
* this configurer, {@link CorsFilter} is not used and {@link PreFlightRequestFilter} is
* registered instead.
*
* @param <H> the builder to return.
* @author Rob Winch
@ -43,6 +46,8 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
private CorsConfigurationSource configurationSource;
private PreFlightRequestHandler preFlightRequestHandler;
/**
* Creates a new instance
*
@ -56,30 +61,85 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
return this;
}
/**
* Use the given {@link PreFlightRequestHandler} for CORS preflight requests. When
* set, {@link CorsFilter} is not used. Cannot be combined with
* {@link #configurationSource(CorsConfigurationSource)}.
* @param preFlightRequestHandler the handler to use
* @return the {@link CorsConfigurer} for additional configuration
*/
public CorsConfigurer<H> preFlightRequestHandler(PreFlightRequestHandler preFlightRequestHandler) {
this.preFlightRequestHandler = preFlightRequestHandler;
return this;
}
@Override
public void configure(H http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
if (this.configurationSource != null && this.preFlightRequestHandler != null) {
throw new IllegalStateException(
"Cannot configure both a CorsConfigurationSource and a PreFlightRequestHandler on CorsConfigurer");
}
CorsFilter corsFilter = getCorsFilter(context);
Assert.state(corsFilter != null, () -> "Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
+ CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
http.addFilter(corsFilter);
if (corsFilter != null) {
http.addFilter(corsFilter);
return;
}
PreFlightRequestHandler preFlightRequestHandlerBean = getPreFlightRequestHandler(context);
if (preFlightRequestHandlerBean != null) {
http.addFilterBefore(new PreFlightRequestFilter(preFlightRequestHandlerBean), CorsFilter.class);
return;
}
throw new NoSuchBeanDefinitionException(CorsConfigurationSource.class,
"Failed to find a bean that implements `CorsConfigurationSource`. Please ensure that you are using "
+ "`@EnableWebMvc`, are publishing a `WebMvcConfigurer`, or are publishing a `CorsConfigurationSource` bean.");
}
private PreFlightRequestHandler getPreFlightRequestHandler(ApplicationContext context) {
if (this.configurationSource != null) {
return null;
}
if (this.preFlightRequestHandler != null) {
return this.preFlightRequestHandler;
}
if (context == null) {
return null;
}
if (context.getBeanNamesForType(PreFlightRequestHandler.class).length > 0) {
return context.getBean(PreFlightRequestHandler.class);
}
return null;
}
private CorsConfigurationSource getCorsConfigurationSource(ApplicationContext context) {
if (context == null) {
return null;
}
boolean containsCorsSource = context.containsBeanDefinition(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
if (containsCorsSource) {
return context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class);
}
return MvcCorsFilter.getMvcCorsConfigurationSource(context);
}
private CorsFilter getCorsFilter(ApplicationContext context) {
if (this.preFlightRequestHandler != null) {
return null;
}
if (this.configurationSource != null) {
return new CorsFilter(this.configurationSource);
}
boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
boolean containsCorsFilter = context != null && context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
if (containsCorsFilter) {
return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
}
boolean containsCorsSource = context.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
if (containsCorsSource) {
CorsConfigurationSource configurationSource = context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME,
CorsConfigurationSource.class);
return new CorsFilter(configurationSource);
CorsConfigurationSource corsConfigurationSource = getCorsConfigurationSource(context);
if (corsConfigurationSource != null) {
return new CorsFilter(corsConfigurationSource);
}
return MvcCorsFilter.getMvcCorsFilter(context);
return null;
}
static class MvcCorsFilter {
@ -92,15 +152,11 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
* @param context
* @return
*/
private static CorsFilter getMvcCorsFilter(ApplicationContext context) {
private static CorsConfigurationSource getMvcCorsConfigurationSource(ApplicationContext context) {
if (context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
CorsConfigurationSource corsConfigurationSource = context
.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, CorsConfigurationSource.class);
return new CorsFilter(corsConfigurationSource);
return context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, CorsConfigurationSource.class);
}
throw new NoSuchBeanDefinitionException(CorsConfigurationSource.class,
"Failed to find a bean that implements `CorsConfigurationSource`. Please ensure that you are using "
+ "`@EnableWebMvc`, are publishing a `WebMvcConfigurer`, or are publishing a `CorsConfigurationSource` bean.");
return null;
}
}

View File

@ -39,6 +39,7 @@ 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.CredentialRecordOwnerAuthorizationManager;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
@ -180,6 +181,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter);
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
rpOperations);
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
rpOperations);
if (creationOptionsRepository != null) {

View File

@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.PreFlightRequestHandler
/**
* A Kotlin DSL to configure [HttpSecurity] CORS using idiomatic Kotlin code.
@ -26,11 +27,14 @@ import org.springframework.web.cors.CorsConfigurationSource
* @author Eleftheria Stein
* @since 5.3
* @property configurationSource the [CorsConfigurationSource] to use.
* @property preFlightRequestHandler the [PreFlightRequestHandler] to use instead of [CorsFilter].
*/
@SecurityMarker
class CorsDsl {
var configurationSource: CorsConfigurationSource? = null
var preFlightRequestHandler: PreFlightRequestHandler? = null
private var disabled = false
/**
@ -42,7 +46,8 @@ class CorsDsl {
internal fun get(): (CorsConfigurer<HttpSecurity>) -> Unit {
return { cors ->
configurationSource?.also { cors.configurationSource(configurationSource) }
configurationSource?.also { cors.configurationSource(it) }
preFlightRequestHandler?.also { cors.preFlightRequestHandler(it) }
if (disabled) {
cors.disable()
}

View File

@ -0,0 +1,122 @@
/*
* Copyright 2004-present the original author 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.authorization;
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.authorization.AuthorizationManagerFactories;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link EnableMultiFactorAuthentication} with
* {@link Customizer}&lt;{@link AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder}&gt;.
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = EnableMultiFactorAuthenticationCustomizerTests.ConfigWithCustomizer.class)
class EnableMultiFactorAuthenticationCustomizerTests {
@Autowired
MockMvc mvc;
@Test
@WithMockUser(username = "user", authorities = "ROLE_USER")
void whenCustomizerAppliedThenConditionalMfaUsed() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", authorities = "ROLE_USER")
void whenCustomizerAppliedAndConditionTrueThenMfaRequired() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "admin", authorities = { "ROLE_USER", FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY })
void whenCustomizerAppliedAndConditionTrueWithMfaThenAuthorized() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isOk());
}
@EnableWebSecurity
@Configuration
@EnableMultiFactorAuthentication(
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
static class ConfigWithCustomizer {
@Bean
Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object>> additionalRequiredFactorsCustomizer() {
return (builder) -> builder.when((auth) -> "admin".equals(auth.getName()));
}
@Bean
MockMvc mvc(WebApplicationContext context) {
return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
}
@Bean
@SuppressWarnings("deprecation")
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@RestController
static class OkController {
@GetMapping("/")
String ok() {
return "ok";
}
}
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2004-present the original author 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.authorization;
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.core.annotation.Order;
import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link EnableMultiFactorAuthentication} with multiple
* {@link Customizer}&lt;{@link AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder}&gt;
* beans.
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(
classes = EnableMultiFactorAuthenticationMultipleCustomizersTests.ConfigWithMultipleCustomizers.class)
class EnableMultiFactorAuthenticationMultipleCustomizersTests {
@Autowired
MockMvc mvc;
@Test
@WithMockUser(username = "user", authorities = "ROLE_USER")
void whenCustomizerAppliedThenConditionalMfaUsed() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", authorities = "ROLE_USER")
void whenCustomizersAppliedAndConditionTrueThenMfaRequired() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "admin", authorities = { "ROLE_USER", FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY })
void whenCustomizersAppliedAndConditionTrueWithMfaThenAuthorized() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isOk());
}
@Test
@WithMockUser(username = "manager", authorities = "ROLE_USER")
void whenSecondCustomizerAppliedAndConditionTrueThenMfaRequired() throws Exception {
this.mvc.perform(get("/")).andExpect(status().isUnauthorized());
}
@EnableWebSecurity
@Configuration
@EnableMultiFactorAuthentication(
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
static class ConfigWithMultipleCustomizers {
@Bean
@Order(1)
Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object>> adminCustomizer() {
return (builder) -> builder.withWhen(
(current) -> (auth) -> "admin".equals(auth.getName()) || (current != null && current.test(auth)));
}
@Bean
@Order(2)
Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object>> managerCustomizer() {
return (builder) -> builder.withWhen(
(current) -> (auth) -> "manager".equals(auth.getName()) || (current != null && current.test(auth)));
}
@Bean
MockMvc mvc(WebApplicationContext context) {
return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
}
@Bean
@SuppressWarnings("deprecation")
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER")
.build();
UserDetails manager = User.withDefaultPasswordEncoder()
.username("manager")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user, admin, manager);
}
@RestController
static class OkController {
@GetMapping("/")
String ok() {
return "ok";
}
}
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2004-present the original author 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.authorization;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MultiFactorAuthenticationSelector}.
*
* @author Rob Winch
*/
class MultiFactorAuthenticationSelectorTests {
private final MultiFactorAuthenticationSelector selector = new MultiFactorAuthenticationSelector();
@Test
void selectImportsWhenWhenIsEmptyAndAuthoritiesSpecifiedThenReturnsImportsWithoutWebAuthnConfig() {
AnnotationMetadata metadata = metadata(new MultiFactorCondition[0], FactorGrantedAuthority.OTT_AUTHORITY,
FactorGrantedAuthority.PASSWORD_AUTHORITY);
String[] imports = this.selector.selectImports(metadata);
assertThat(imports).isNotEmpty();
assertThat(imports).doesNotContain(WhenWebAuthnRegisteredMfaConfiguration.class.getName());
}
@Test
void selectImportsWhenWhenOmittedThenDefaultsToEmptyAndReturnsImports() {
AnnotationMetadata metadata = metadataWithoutWhen(FactorGrantedAuthority.OTT_AUTHORITY,
FactorGrantedAuthority.PASSWORD_AUTHORITY);
String[] imports = this.selector.selectImports(metadata);
assertThat(imports).isNotEmpty();
assertThat(imports).doesNotContain(WhenWebAuthnRegisteredMfaConfiguration.class.getName());
}
@Test
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesIncludesWebAuthnThenReturnsImports() {
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED },
FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.WEBAUTHN_AUTHORITY);
String[] imports = this.selector.selectImports(metadata);
assertThat(imports).isNotEmpty();
}
@Test
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesOnlyWebAuthnThenReturnsImports() {
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED },
FactorGrantedAuthority.WEBAUTHN_AUTHORITY);
String[] imports = this.selector.selectImports(metadata);
assertThat(imports).isNotEmpty();
}
@Test
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesEmptyThenThrowsException() {
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED });
assertThatIllegalArgumentException().isThrownBy(() -> this.selector.selectImports(metadata))
.withMessageContaining("authorities() must include " + FactorGrantedAuthority.WEBAUTHN_AUTHORITY);
}
@Test
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesExcludesWebAuthnThenThrowsException() {
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED },
FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY);
assertThatIllegalArgumentException().isThrownBy(() -> this.selector.selectImports(metadata))
.withMessageContaining("authorities() must include " + FactorGrantedAuthority.WEBAUTHN_AUTHORITY);
}
private static AnnotationMetadata metadata(MultiFactorCondition[] when, String... authorities) {
AnnotationMetadata metadata = mock(AnnotationMetadata.class);
Map<String, Object> attrs = new HashMap<>();
attrs.put("authorities", authorities);
attrs.put("when", when);
given(metadata.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName())).willReturn(attrs);
return metadata;
}
private static AnnotationMetadata metadataWithoutWhen(String... authorities) {
AnnotationMetadata metadata = mock(AnnotationMetadata.class);
Map<String, Object> attrs = new HashMap<>();
attrs.put("authorities", authorities);
given(metadata.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName())).willReturn(attrs);
return metadata;
}
}

View File

@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.PreFlightRequestHandler;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ -196,6 +197,73 @@ public class CorsConfigurerTests {
.andExpect(header().exists("X-Content-Type-Options"));
}
@Test
public void optionsWhenPreFlightRequestHandlerBeanThenHandled() throws Exception {
this.spring.register(PreFlightRequestHandlerConfig.class).autowire();
this.mvc
.perform(options("/")
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk())
.andExpect(header().exists("X-Pre-Flight"));
}
@Test
public void optionsWhenNoPreFlightRequestHandlerBeanThenCorsFilterUsed() throws Exception {
this.spring.register(NoPreFlightRequestHandlerConfig.class).autowire();
this.mvc
.perform(options("/")
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk())
.andExpect(header().exists("Access-Control-Allow-Origin"))
.andExpect(header().doesNotExist("X-Pre-Flight"));
}
@Test
public void optionsWhenExplicitConfigurationSourceThenPreFlightRequestHandlerBeanIgnored() throws Exception {
this.spring.register(ExplicitConfigurationSourceWithPreFlightRequestHandlerBeanConfig.class).autowire();
this.mvc
.perform(options("/")
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk())
.andExpect(header().exists("Access-Control-Allow-Origin"))
.andExpect(header().doesNotExist("X-Pre-Flight"));
}
@Test
public void optionsWhenPreFlightRequestHandlerMemberThenHandled() throws Exception {
this.spring.register(PreFlightRequestHandlerMemberConfig.class).autowire();
this.mvc
.perform(options("/")
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk())
.andExpect(header().exists("X-Pre-Flight"));
}
@Test
public void configureWhenBothConfigurationSourceAndPreFlightRequestHandlerMemberThenIllegalState() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(BothCorsConfigurerMembersConfig.class).autowire())
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessageContaining("Cannot configure both");
}
@Test
public void optionsWhenPreFlightRequestHandlerMemberThenCorsFilterBeanIgnored() throws Exception {
this.spring.register(PreFlightRequestHandlerMemberWithCorsFilterBeanConfig.class).autowire();
this.mvc
.perform(options("/")
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk())
.andExpect(header().exists("X-Pre-Flight"))
.andExpect(header().doesNotExist("Access-Control-Allow-Origin"));
}
@Configuration
@EnableWebSecurity
static class DefaultCorsConfig {
@ -382,4 +450,150 @@ public class CorsConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class PreFlightRequestHandlerConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated())
.cors(withDefaults());
return http.build();
// @formatter:on
}
@Bean
PreFlightRequestHandler preFlightRequestHandler() {
return (request, response) -> response.addHeader("X-Pre-Flight", "Handled");
}
}
@Configuration
@EnableWebSecurity
static class NoPreFlightRequestHandlerConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated())
.cors(withDefaults());
return http.build();
// @formatter:on
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
@Configuration
@EnableWebSecurity
static class ExplicitConfigurationSourceWithPreFlightRequestHandlerBeanConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
source.registerCorsConfiguration("/**", corsConfiguration);
// @formatter:off
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated())
.cors((cors) -> cors.configurationSource(source));
return http.build();
// @formatter:on
}
@Bean
PreFlightRequestHandler preFlightRequestHandler() {
return (request, response) -> response.addHeader("X-Pre-Flight", "Handled");
}
}
@Configuration
@EnableWebSecurity
static class PreFlightRequestHandlerMemberConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated())
.cors((cors) -> cors.preFlightRequestHandler(
(request, response) -> response.addHeader("X-Pre-Flight", "Member")));
return http.build();
// @formatter:on
}
}
@Configuration
@EnableWebSecurity
static class BothCorsConfigurerMembersConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
source.registerCorsConfiguration("/**", corsConfiguration);
// @formatter:off
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated())
.cors((cors) -> cors
.configurationSource(source)
.preFlightRequestHandler((request, response) -> response.addHeader("X-Pre-Flight", "Handled")));
return http.build();
// @formatter:on
}
}
@Configuration
@EnableWebSecurity
static class PreFlightRequestHandlerMemberWithCorsFilterBeanConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated())
.cors((cors) -> cors.preFlightRequestHandler(
(request, response) -> response.addHeader("X-Pre-Flight", "Member")));
return http.build();
// @formatter:on
}
@Bean
CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
}

View File

@ -43,9 +43,16 @@ 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.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
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.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
import org.springframework.test.web.servlet.MockMvc;
@ -58,6 +65,7 @@ import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@ -257,6 +265,24 @@ public class WebAuthnConfigurerTests {
.andExpect(content().string(expectedBody));
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
.andExpect(status().isNoContent());
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
.andExpect(status().isForbidden());
}
@Configuration
@EnableWebSecurity
static class ConfigCredentialCreationOptionsRepository {
@ -475,4 +501,47 @@ public class WebAuthnConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class DeleteCredentialConfiguration {
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
@Bean
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
return mock(WebAuthnRelyingPartyOperations.class);
}
@Bean
UserCredentialRepository userCredentialRepository() {
MapUserCredentialRepository repository = new MapUserCredentialRepository();
repository.save(TestCredentialRecords.userCredential().build());
return repository;
}
@Bean
PublicKeyCredentialUserEntityRepository userEntityRepository() {
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(USER_ENTITY_ID)
.displayName("User")
.build());
return repository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
}
}
}

View File

@ -157,7 +157,7 @@ public class ClientRegistrationsBeanDefinitionParserTests {
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(googleRegistration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(googleRegistration.getRedirectUri()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}");
assertThat(googleRegistration.getScopes()).isNull();
assertThat(googleRegistration.getScopes()).isEmpty();
assertThat(googleRegistration.getClientName()).isEqualTo(serverUrl);
ProviderDetails googleProviderDetails = googleRegistration.getProviderDetails();
assertThat(googleProviderDetails).isNotNull();

View File

@ -16,7 +16,10 @@
package org.springframework.security.config.annotation.web
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.Assertions.catchThrowable
import org.assertj.core.util.Throwables
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.BeanCreationException
@ -35,9 +38,14 @@ import org.springframework.test.web.servlet.get
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.PreFlightRequestHandler
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.filter.CorsFilter
import org.springframework.web.servlet.config.annotation.EnableWebMvc
/**
@ -153,7 +161,7 @@ class CorsDslTests {
@Test
fun `CORS when CORS configuration source dsl then responds with CORS header`() {
this.spring.register(CorsCrossOriginBeanConfig::class.java, HomeController::class.java).autowire()
this.spring.register(CorsCrossOriginSourceConfig::class.java, HomeController::class.java).autowire()
this.mockMvc.get("/")
{
@ -185,6 +193,117 @@ class CorsDslTests {
}
}
@Test
fun `CORS when preFlight request handler dsl then OPTIONS uses handler`() {
this.spring.register(PreFlightRequestHandlerDslConfig::class.java).autowire()
this.mockMvc.perform(options("/")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, RequestMethod.POST.name)
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk)
.andExpect(header().exists("X-Pre-Flight"))
}
@Configuration
@EnableWebSecurity
open class PreFlightRequestHandlerDslConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
cors {
preFlightRequestHandler = PreFlightRequestHandler { _, response ->
response.addHeader("X-Pre-Flight", "Dsl")
}
}
}
return http.build()
}
}
@Test
fun `CORS when configuration source and preFlight handler dsl then illegal state`() {
val thrown = catchThrowable {
this.spring.register(BothCorsDslMembersConfig::class.java).autowire()
}
assertThat(thrown).isInstanceOf(BeanCreationException::class.java)
assertThat(Throwables.getRootCause(thrown))
.isInstanceOf(IllegalStateException::class.java)
.hasMessageContaining("Cannot configure both")
}
@Configuration
@EnableWebSecurity
open class BothCorsDslMembersConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val source = UrlBasedCorsConfigurationSource()
val corsConfiguration = CorsConfiguration()
corsConfiguration.allowedOrigins = listOf("*")
corsConfiguration.allowedMethods = listOf(
RequestMethod.GET.name,
RequestMethod.POST.name)
source.registerCorsConfiguration("/**", corsConfiguration)
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
cors {
configurationSource = source
preFlightRequestHandler = PreFlightRequestHandler { _, response ->
response.addHeader("X-Pre-Flight", "Dsl")
}
}
}
return http.build()
}
}
@Test
fun `CORS when preFlight handler dsl then CorsFilter bean ignored on OPTIONS`() {
this.spring.register(PreFlightRequestHandlerDslWithCorsFilterBeanConfig::class.java).autowire()
this.mockMvc.perform(options("/")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, RequestMethod.POST.name)
.header(HttpHeaders.ORIGIN, "https://example.com"))
.andExpect(status().isOk)
.andExpect(header().exists("X-Pre-Flight"))
.andExpect(header().doesNotExist("Access-Control-Allow-Origin"))
}
@Configuration
@EnableWebSecurity
open class PreFlightRequestHandlerDslWithCorsFilterBeanConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
cors {
preFlightRequestHandler = PreFlightRequestHandler { _, response ->
response.addHeader("X-Pre-Flight", "Dsl")
}
}
}
return http.build()
}
@Bean
open fun corsFilter(): CorsFilter {
val source = UrlBasedCorsConfigurationSource()
val corsConfiguration = CorsConfiguration()
corsConfiguration.allowedOrigins = listOf("*")
corsConfiguration.allowedMethods = listOf(
RequestMethod.GET.name,
RequestMethod.POST.name)
source.registerCorsConfiguration("/**", corsConfiguration)
return CorsFilter(source)
}
}
@RestController
private class HomeController {
@GetMapping("/")

View File

@ -73,13 +73,11 @@ class AuthorizationEndpointDslTests {
companion object {
val RESOLVER: OAuth2AuthorizationRequestResolver = object : OAuth2AuthorizationRequestResolver {
override fun resolve(
request: HttpServletRequest?
) = OAuth2AuthorizationRequest.authorizationCode().build()
override fun resolve(request: HttpServletRequest) =
OAuth2AuthorizationRequest.authorizationCode().build()
override fun resolve(
request: HttpServletRequest?, clientRegistrationId: String?
) = OAuth2AuthorizationRequest.authorizationCode().build()
override fun resolve(request: HttpServletRequest, clientRegistrationId: String) =
OAuth2AuthorizationRequest.authorizationCode().build()
}
}

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