1
0
mirror of synced 2026-09-04 08:19:54 +00:00

Compare commits

...

120 Commits

Author SHA1 Message Date
Manuel Siggen 29f8dd53cd Pick Up PermissionEvaluator Bean
Closes gh-19084

Signed-off-by: Manuel Siggen <manuel@siggen.ch>
2026-09-03 16:57:46 -07:00
Josh Cummings 7526dfa694 Merge branch '7.1.x' 2026-09-03 15:42:38 -06:00
Josh Cummings 02f5f5a459 Merge branch '7.0.x' into 7.1.x 2026-09-03 15:42:29 -06:00
dependabot[bot] 3dff446d18 Bump spring-io/spring-release-actions/get-todays-release-version
Bumps [spring-io/spring-release-actions/get-todays-release-version](https://github.com/spring-io/spring-release-actions) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/spring-io/spring-release-actions/releases)
- [Commits](https://github.com/spring-io/spring-release-actions/compare/a1f321783a0769dd2aea4fad6c2ae2f95a52b885...1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95)

---
updated-dependencies:
- dependency-name: spring-io/spring-release-actions/get-todays-release-version
  dependency-version: 0.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:39:30 -07:00
dependabot[bot] cc89707b03 Bump antora from 3.2.0-rc.2 to 3.2.0-rc.3 in /docs
Bumps [antora](https://gitlab.com/antora/antora) from 3.2.0-rc.2 to 3.2.0-rc.3.
- [Changelog](https://gitlab.com/antora/antora/blob/main/CHANGELOG.adoc)
- [Commits](https://gitlab.com/antora/antora/compare/v3.2.0-rc.2...v3.2.0-rc.3)

---
updated-dependencies:
- dependency-name: antora
  dependency-version: 3.2.0-rc.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:38:41 -07:00
dependabot[bot] 812ad9c547 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.15 to 1.0.17.
- [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/b92832ecbc7cbe969201e6beafbde0ee400cf095...3f6cc7ffc137ca160061749d5f34dc30d5f36986)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:38:08 -07:00
dependabot[bot] 364c153e17 Bump spring-io/spring-release-actions/compute-next-version
Bumps [spring-io/spring-release-actions/compute-next-version](https://github.com/spring-io/spring-release-actions) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/spring-io/spring-release-actions/releases)
- [Commits](https://github.com/spring-io/spring-release-actions/compare/a1f321783a0769dd2aea4fad6c2ae2f95a52b885...1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95)

---
updated-dependencies:
- dependency-name: spring-io/spring-release-actions/compute-next-version
  dependency-version: 0.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:37:16 -07:00
dependabot[bot] 39383ab970 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 ed473b4dafba053c63a453d2d88a89df3b3e18b3 to 9ca0acea761aa79d71c78cc462048c49ed5c4230.
- [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/ed473b4dafba053c63a453d2d88a89df3b3e18b3...9ca0acea761aa79d71c78cc462048c49ed5c4230)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/test.yml
  dependency-version: 9ca0acea761aa79d71c78cc462048c49ed5c4230
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:35:20 -07:00
Tran Ngoc Nhan 8ef5bcd003 Fix typos in Javadocs
- Remove duplicate words
- Remove unnecessary parentheses
- Fix grammar

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-09-03 14:34:29 -07:00
dependabot[bot] 4193b73d47 Bump actions/setup-java from 5.7.0 to 6.0.0
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 5.7.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/b6effb05e454b25005698d916606bdc6ffcbf961...dd06d9cba3e5552c54d9f8ea23572deb30010f7c)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:33:13 -07:00
dependabot[bot] 4ce48297f6 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.15 to 1.0.17.
- [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.15...v1.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:32:07 -07:00
Josh Cummings d09463b865 Merge branch '7.1.x'
Closes gh-19649
2026-09-03 15:28:33 -06:00
Josh Cummings 9989735a49 Merge branch '7.0.x' into 7.1.x
Closes gh-19648
2026-09-03 15:22:26 -06:00
Josh Cummings c7be397a16 Merge branch '7.1.x'
# Conflicts:
#	docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc
#	docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc
2026-09-03 15:14:01 -06:00
Tran Ngoc Nhan 02cc2e9d14 Fix TokenType Comparison Logic
Compare OAuth2AccessToken.TokenType using equals() instead of == in
BearerTokenAuthentication, since TokenType instances are not
guaranteed to be singletons and reference comparison can incorrectly
reject an otherwise-equal bearer token.

Closes gh-19377

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-09-03 14:06:08 -07:00
Josh Cummings 118e51d45e Merge branch '7.0.x' into 7.1.x 2026-09-03 15:01:11 -06:00
Tran Ngoc Nhan 27a76a67ae Include code example for reactive onetimetoken
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-09-03 14:00:44 -07:00
Tran Ngoc Nhan ff6cef0cd2 Include code example for servlet onetimetoken
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-09-03 14:00:44 -07:00
Josh Cummings 2e52e1063a Merge branch '7.1.x'
Closes gh-19646
2026-09-03 12:27:03 -06:00
Josh Cummings 801f890ed0 Merge branch '7.0.x' into 7.1.x
Closes gh-9175 on 7.1.x
2026-09-03 12:26:14 -06:00
Josh Cummings 1a7769be28 Fix Checkstyle
Issue gh-9175

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-09-03 11:18:58 -07:00
Dmitrii Bocharov bdf501fe87 Fix HeaderWriterFilter Race Condition
Closes gh-9175

Signed-off-by: Dmitrii Bocharov <bdshadow@gmail.com>
2026-09-03 11:18:58 -07:00
avsan b26d968c1c Simplify ProviderManager null provider check
Use Assert.noNullElements to validate configured AuthenticationProviders
without relying on CollectionUtils.contains.

Preserve the existing null-provider validation behavior.

Closes gh-19510

Signed-off-by: avsan <abhishekrajabca@gmail.com>
2026-09-03 11:10:09 -07:00
Josh Cummings 3070c96594 Add authenticationSuccessHandler to Reactive Resource Server Kotlin DSL
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-09-01 18:14:07 -07:00
Josh Cummings 6eb7ef6e6d Update What's New
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-09-01 18:14:07 -07:00
Josh Cummings 988ba6b33c Align Formatting
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-09-01 18:14:07 -07:00
Iain Henderson d1fb6141c1 Add authenticationSuccessHandler to Reactive Resource Server DSL
Signed-off-by: Iain Henderson <Iain.henderson@mac.com>
2026-09-01 18:14:07 -07:00
Josh Cummings 43f5ae5b21 Remove Auto-merge for .x Branches
Closes gh-19632

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-31 10:43:16 -06:00
Josh Cummings b2a6bbf314 Merge branch '7.1.x' 2026-08-31 10:42:04 -06:00
Josh Cummings a161421409 Turn Off Auto-merge for Maintenance Branch
This commit removes the auto-merge workflow on 7.1.x since it
is a maintenance branch.

Closes gh-19631

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-31 10:41:54 -06:00
Joe Grandja 36aa2ad744 Merge branch '7.1.x' 2026-08-31 12:02:21 -04:00
Josh Cummings ed4e0e2ea8 Merge branch '7.1.x' 2026-08-31 09:55:00 -06:00
Joe Grandja 978eb4396a Polish gh-19585 2026-08-31 11:09:18 -04:00
Evgeniy Cheban 770c860d9d Ensure WebSession ID is not changed after token refresh (Reactive)
Closes gh-19424

Signed-off-by: Evgeniy Cheban <mister.cheban@gmail.com>
2026-08-31 10:11:13 -04:00
dependabot[bot] 11890f2f49 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.16 to 1.0.17.
- [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.16...v1.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 03:17:31 +00:00
dependabot[bot] 5809451de7 Bump spring-io/spring-release-actions/compute-next-version
Bumps [spring-io/spring-release-actions/compute-next-version](https://github.com/spring-io/spring-release-actions) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/spring-io/spring-release-actions/releases)
- [Commits](https://github.com/spring-io/spring-release-actions/compare/a1f321783a0769dd2aea4fad6c2ae2f95a52b885...1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95)

---
updated-dependencies:
- dependency-name: spring-io/spring-release-actions/compute-next-version
  dependency-version: 0.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 00:50:20 +00:00
dependabot[bot] 31d379a7bf Bump spring-io/spring-security-release-tools/.github/workflows/release-announcements-stage.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/release-announcements-stage.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.16 to 1.0.17.
- [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/d6c65d3013c0888e2c9cbae9f4beda610994776c...3f6cc7ffc137ca160061749d5f34dc30d5f36986)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 00:47:19 +00:00
dependabot[bot] 825370f8e3 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.15 to 1.0.17.
- [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.15...3f6cc7ffc137ca160061749d5f34dc30d5f36986)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 00:47:16 +00:00
Josh Cummings 61feae94a0 Merge branch '7.1.x' 2026-08-26 10:17:45 -06:00
Josh Cummings edaae1d17d Merge branch '7.0.x' into 7.1.x 2026-08-26 10:17:34 -06:00
dependabot[bot] e8084090ff Bump org.hibernate.orm:hibernate-core from 7.4.5.Final to 7.4.6.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 7.4.5.Final to 7.4.6.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/7.4.6/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/7.4.5...7.4.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 03:24:42 +00:00
dependabot[bot] 4773b40f02 Bump gradle-wrapper from 9.7.0 to 9.7.1
Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.7.0 to 9.7.1.
- [Release notes](https://github.com/gradle/gradle/releases)
- [Commits](https://github.com/gradle/gradle/compare/v9.7.0...v9.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 03:28:40 +00:00
dependabot[bot] 9a4a965b0d 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.15 to 1.0.16.
- [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.15...v1.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 03:27:38 +00:00
Josh Cummings 0bae8a73d1 Next Development Version
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-21 09:14:37 -06:00
Josh Cummings 461ccd817d Next Development Version
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-21 09:14:09 -06:00
Josh Cummings d449950b36 Merge branch '7.1.x'
Closes gh-19573
2026-08-20 16:58:15 -06:00
Josh Cummings 8dc341af8c Merge branch '7.0.x' into 7.1.x
Closes gh-19572
2026-08-20 16:58:02 -06:00
Josh Cummings 381c556f78 Add Release Announcement Workflows
Closes gh-19571

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 16:57:48 -06:00
Josh Cummings 19afc9121e Merge branch '7.1.x'
Closes gh-19568
# Conflicts:
#	gradle/libs.versions.toml
2026-08-20 12:29:12 -06:00
Josh Cummings 6b2f892e66 Merge branch '7.0.x' into 7.1.x
Closes gh-19567
# Conflicts:
#	config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java
#	config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
#	config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationTests.java
#	gradle/libs.versions.toml
#	oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java
#	oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java
#	oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofReplayValidator.java
#	oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java
2026-08-20 12:23:20 -06:00
Josh Cummings 3b276e6c0d Update to Spring Data 2025.1.7
Closes gh-19485

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:24:08 -06:00
Josh Cummings f2115fbef8 Update to Spring LDAP 4.0.5
Closes gh-19486

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:24:01 -06:00
Josh Cummings 298335a57e Update to Micrometer 1.16.7
Closes gh-19484

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:23:55 -06:00
Josh Cummings c6566ee772 Update to Reactor 2025.0.7
Closes gh-19483

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:23:50 -06:00
Josh Cummings 7442214225 Update to Spring Framework 7.0.9
Closes gh-19482

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:23:44 -06:00
Josh Cummings 0123ae0981 Deprecate AesBytesEncryptor
This commit separates AesBytesEncryptor into two separate
implememtations, allowing for a migration away from default
arrangements that used a null IV

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:23:36 -06:00
Josh Cummings 11a648d106 Improve Equivalence Tests
This commit adds equals and hashCode implementations as well
as a readResolve implementation to ensure that deserialization
mechanisms can correctly assess the equality of a constnat
and a corresponding deserialized instance. For defense-in-depth
reasons, this commit also favors .equals over == for these
constants.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:23:29 -06:00
Joe Grandja 536e09756c Bind UnboundIdContainer to loopback address 2026-08-20 11:23:22 -06:00
Joe Grandja ac15cdbea3 Apply html encoding in DefaultConsentPage 2026-08-20 11:23:14 -06:00
Joe Grandja ad812aefc4 Provide ability to configure DPoP proof replay 2026-08-20 11:23:08 -06:00
Josh Cummings d8769fb183 Use Constant-Time Comparison
This commit updates password encoders to use a
constant-time comparison method to defend against
timing attacks

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:23:02 -06:00
Josh Cummings ae997a4f46 Use Utf8#isEqual
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:22:55 -06:00
Josh Cummings bbc6273abf Use Constant-Time Equals
This commit updates the filter to use constant-time
equals for sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:22:49 -06:00
Josh Cummings 475af0d3db Use Constant-Time Equals
This commit updates the filter to use constant-time
equals for sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:22:43 -06:00
Josh Cummings f8a61e66d1 Add Utf8#isEqual
This commit adds a constant-time equals method,
useful for comparing password hashes or other
sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:22:36 -06:00
Josh Cummings 8830e39073 Use MessageDigest#isEqual
This commit favors constant-time comparison
to mitigate timing attacks

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:22:31 -06:00
Josh Cummings 304cc21523 Add Tests for User and Device Code
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:22:25 -06:00
Josh Cummings 7048b08b9c Update to Spring Data 2026.0.1
Closes gh-19490

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:29 -06:00
Josh Cummings ff108a49a5 Update to Spring LDAP 4.1.1
Closes gh-19491

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:29 -06:00
Josh Cummings 9a70d73036 Update to Micrometer 1.17.1
Closes gh-19489

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:28 -06:00
Josh Cummings cf613ad2d3 Update to Reactor 2025.0.7
Closes gh-19488

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:28 -06:00
Josh Cummings fdec2d7617 Update to Spring Framework 7.0.9
Closes gh-19487

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:28 -06:00
Josh Cummings 748e3c9cf0 Use jspecify Nullable in DPoPProofReplayValidator
org.springframework.lang.Nullable is banned by the
bannedNullabilityImports checkstyle rule on this line; the embargoed
commit that introduced this file predates that rule's adoption here.
Switch to org.jspecify.annotations.Nullable to match the rest of the
codebase (already used correctly elsewhere in this same file).

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:21 -06:00
Josh Cummings 057581584a Align Null Behavior
Previously, ExternalInetAddressMatcher treated a null InetAddress
as external by negating InternalInetAddressMatcher's result.

This commit makes ExternalInetAddressMatcher return false for a
null address, so that neither the internal nor the external
matcher classifies an unknown address as a match.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:20 -06:00
Josh Cummings 502ecf9241 Deprecate AesBytesEncryptor
This commit separates AesBytesEncryptor into two separate
implememtations, allowing for a migration away from default
arrangements that used a null IV

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:20 -06:00
Josh Cummings 7db67f923c Improve Equivalence Tests
This commit adds equals and hashCode implementations as well
as a readResolve implementation to ensure that deserialization
mechanisms can correctly assess the equality of a constnat
and a corresponding deserialized instance. For defense-in-depth
reasons, this commit also favors .equals over == for these
constants.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:14 -06:00
Joe Grandja 35859220cb Bind UnboundIdContainer to loopback address 2026-08-20 11:21:14 -06:00
Joe Grandja 3836091fe2 Apply html encoding in DefaultConsentPage 2026-08-20 11:21:14 -06:00
Joe Grandja 4c41928bc7 Provide ability to configure DPoP proof replay 2026-08-20 11:21:13 -06:00
Josh Cummings 61be628ad6 Use Constant-Time Comparison
This commit updates password encoders to use a
constant-time comparison method to defend against
timing attacks

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:06 -06:00
Josh Cummings e6208a4832 Use Utf8#isEqual
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:06 -06:00
Josh Cummings 01452e328f Use Constant-Time Equals
This commit updates the filter to use constant-time
equals for sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:05 -06:00
Josh Cummings 836190546d Use Constant-Time Equals
This commit updates the filter to use constant-time
equals for sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:21:05 -06:00
Josh Cummings 9f94fb92c5 Add Utf8#isEqual
This commit adds a constant-time equals method,
useful for comparing password hashes or other
sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:20:58 -06:00
Josh Cummings 47804ee834 Use MessageDigest#isEqual
This commit favors constant-time comparison
to mitigate timing attacks

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:20:58 -06:00
Josh Cummings 865085e0b0 Add Tests for User and Device Code
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:20:58 -06:00
Josh Cummings 15a31d7dd7 Check isAnyLocalAddress
This commit updates InternalInetAddressMatcher to check
InetAddress#isAnyLocalAddress in order to catch
additional IP addresses

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 11:20:58 -06:00
Josh Cummings 1fe59232f0 Update to Spring Data 2026.1.0-M1
Closes gh-19531

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:10 -06:00
Josh Cummings 8f13ab216f Update to Micrometer 1.18.0-M1
Closes gh-19530

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:09 -06:00
Josh Cummings 8beb08974b Update to Reactor 2026.0.0-M1
Closes gh-19529

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:09 -06:00
Josh Cummings 49c89546c6 Update to Spring Framework 7.1.0-M1
Closes gh-19528

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:09 -06:00
Josh Cummings 5a74026036 Use jspecify Nullable in DPoPProofReplayValidator
org.springframework.lang.Nullable is banned by the
bannedNullabilityImports checkstyle rule on this line; the embargoed
commit that introduced this file predates that rule's adoption here.
Switch to org.jspecify.annotations.Nullable to match the rest of the
codebase (already used correctly elsewhere in this same file).

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:09 -06:00
Josh Cummings c9220ca39e Align Null Behavior
Previously, ExternalInetAddressMatcher treated a null InetAddress
as external by negating InternalInetAddressMatcher's result.

This commit makes ExternalInetAddressMatcher return false for a
null address, so that neither the internal nor the external
matcher classifies an unknown address as a match.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:09 -06:00
Josh Cummings b6b9d743a0 Deprecate AesBytesEncryptor
This commit separates AesBytesEncryptor into two separate
implememtations, allowing for a migration away from default
arrangements that used a null IV

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:08 -06:00
Josh Cummings 8ded9dac6a Improve Equivalence Tests
This commit adds equals and hashCode implementations as well
as a readResolve implementation to ensure that deserialization
mechanisms can correctly assess the equality of a constnat
and a corresponding deserialized instance. For defense-in-depth
reasons, this commit also favors .equals over == for these
constants.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:08 -06:00
Joe Grandja 1b84c74776 Bind UnboundIdContainer to loopback address 2026-08-20 10:38:08 -06:00
Joe Grandja 83dfc0790e Apply html encoding in DefaultConsentPage 2026-08-20 10:38:08 -06:00
Joe Grandja b55f66695e Provide ability to configure DPoP proof replay 2026-08-20 10:38:08 -06:00
Josh Cummings 177e482783 Use Constant-Time Comparison
This commit updates password encoders to use a
constant-time comparison method to defend against
timing attacks

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:07 -06:00
Josh Cummings 04d95818e3 Use Utf8#isEqual
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:07 -06:00
Josh Cummings 627a412748 Use Constant-Time Equals
This commit updates the filter to use constant-time
equals for sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:07 -06:00
Josh Cummings addebb61ed Use Constant-Time Equals
This commit updates the filter to use constant-time
equals for sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:07 -06:00
Josh Cummings 75d985fb13 Add Utf8#isEqual
This commit adds a constant-time equals method,
useful for comparing password hashes or other
sensitive material

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:07 -06:00
Josh Cummings 342b24d43e Use MessageDigest#isEqual
This commit favors constant-time comparison
to mitigate timing attacks

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:07 -06:00
Josh Cummings 2f97d8dac2 Add Tests for User and Device Code
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-20 10:38:06 -06:00
dependabot[bot] 9d8026fcf3 Bump com.fasterxml.jackson:jackson-bom from 2.22.1 to 2.22.2
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.22.1 to 2.22.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.22.1...jackson-bom-2.22.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 03:19:14 +00:00
dependabot[bot] 3881de5d6c Bump com.fasterxml.jackson:jackson-bom from 2.22.1 to 2.22.2
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.22.1 to 2.22.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.22.1...jackson-bom-2.22.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 03:17:09 +00:00
Josh Cummings 1fa37a9479 Merge branch '7.1.x' 2026-08-19 16:55:49 -06:00
Josh Cummings 4e92ea215c Merge branch '7.0.x' into 7.1.x 2026-08-19 16:55:38 -06:00
Tadaya Tsuyukubo d111029078 Prevent sharing SecurityContext across threads
`SecurityContextHolderThreadLocalAccessor` currently propagates the
same `SecurityContext` to other threads when Micrometer
Context Propagation is used. This leads to unintended sharing of
mutable state and can cause authentication to leak between threads.

This change updates the accessor to create a new
`SecurityContext` for the target thread while reusing only the
`Authentication` value. Each thread now receives its own
`SecurityContext` instance, preventing cross-thread interference and
aligning with recommended `SecurityContext` usage.

Signed-off-by: Tadaya Tsuyukubo <tadaya@ttddyy.net>
2026-08-19 16:54:43 -06:00
Josh Cummings d7ea83017a Make MockSecurityContextHolderStrategy Thread-safe
Closes gh-19547

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-19 16:54:12 -06:00
Joe Grandja 6401fde9f2 Merge branch '7.1.x' 2026-08-18 12:17:53 -04:00
Joe Grandja 7491e37310 Polish gh-19421 2026-08-18 12:11:09 -04:00
Peter Phillips d90714f07d Fix to allow null servlet request/response in ServletOAuth2AuthorizedClientExchangeFilterFunction
Issue gh-17819

Closes gh-19421

Signed-off-by: Peter Phillips <5099053+petergphillips@users.noreply.github.com>
2026-08-18 11:32:07 -04:00
dependabot[bot] 3cf0867955 Bump tools.jackson:jackson-bom from 3.2.1 to 3.2.2
Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.2.1 to 3.2.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.2.1...jackson-bom-3.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 03:22:02 +00:00
dependabot[bot] 7f219ea530 Bump ch.qos.logback:logback-classic from 1.6.2 to 1.6.3
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.6.2...v_1.6.3)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 03:19:31 +00:00
dependabot[bot] b622028f84 Bump tools.jackson:jackson-bom from 3.2.1 to 3.2.2
Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.2.1 to 3.2.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.2.1...jackson-bom-3.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 03:16:54 +00:00
dependabot[bot] 8b92e71cb2 Bump ch.qos.logback:logback-classic from 1.6.2 to 1.6.3
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.6.2...v_1.6.3)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 03:16:27 +00:00
dependabot[bot] 2c0c09b9a6 Bump ch.qos.logback:logback-classic from 1.6.1 to 1.6.2
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.6.1...v_1.6.2)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 03:27:57 +00:00
dependabot[bot] 6ed154527e Bump ch.qos.logback:logback-classic from 1.6.1 to 1.6.2
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.6.1...v_1.6.2)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 03:18:43 +00:00
137 changed files with 5077 additions and 1240 deletions
+1 -2
View File
@@ -4,7 +4,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- '*.x'
- 'docs-build' - 'docs-build'
run-name: Merge Dependabot PR ${{ github.ref_name }} run-name: Merge Dependabot PR ${{ github.ref_name }}
@@ -14,4 +13,4 @@ jobs:
permissions: write-all permissions: write-all
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@0d3f15bb384839966a1ff5c4383731a2b747f24b # v7 uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@0d3f15bb384839966a1ff5c4383731a2b747f24b # v7
with: with:
mergeArguments: --auto --rebase mergeArguments: --auto --rebase
@@ -17,7 +17,7 @@ permissions:
jobs: jobs:
build: build:
name: Build name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15 uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest, windows-latest ] os: [ ubuntu-latest, windows-latest ]
@@ -39,7 +39,7 @@ jobs:
deploy-schema: deploy-schema:
name: Deploy Schema name: Deploy Schema
needs: [ build ] needs: [ build ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15 uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with: with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }} should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
java-version: 25 java-version: 25
+2 -2
View File
@@ -21,14 +21,14 @@ jobs:
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5 uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
- name: Get Today's Release Version - name: Get Today's Release Version
id: todays-release id: todays-release
uses: spring-io/spring-release-actions/get-todays-release-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5 uses: spring-io/spring-release-actions/get-todays-release-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
with: with:
snapshot-version: ${{ steps.compute-version.outputs.version }} snapshot-version: ${{ steps.compute-version.outputs.version }}
milestone-repository: ${{ github.repository }} milestone-repository: ${{ github.repository }}
milestone-token: ${{ secrets.GITHUB_TOKEN }} milestone-token: ${{ secrets.GITHUB_TOKEN }}
- name: Compute Next Version - name: Compute Next Version
id: next-version id: next-version
uses: spring-io/spring-release-actions/compute-next-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5 uses: spring-io/spring-release-actions/compute-next-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
with: with:
version: ${{ steps.todays-release.outputs.release-version }} version: ${{ steps.todays-release.outputs.release-version }}
- name: Schedule Next Milestone - name: Schedule Next Milestone
@@ -22,7 +22,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up JDK 25 - name: Set up JDK 25
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with: with:
java-version: '25' java-version: '25'
distribution: 'temurin' distribution: 'temurin'
@@ -0,0 +1,30 @@
name: Release Announcements - Publish
on:
workflow_dispatch:
inputs:
version:
description: The version to publish
required: true
type: string
permissions:
actions: write
contents: read
issues: write
jobs:
deploy-schema:
name: Deploy Schema
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
should-deploy-schema: true
secrets: inherit
publish-release-announcements:
name: Publish Release Announcements
needs: [ deploy-schema ]
if: ${{ !github.event.repository.fork }}
uses: spring-io/spring-security-release-tools/.github/workflows/release-announcements-publish.yml@d6c65d3013c0888e2c9cbae9f4beda610994776c # v1.0.16
with:
version: ${{ inputs.version }}
secrets: inherit
@@ -0,0 +1,24 @@
name: Release Announcements - Stage
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
version:
description: The version to stage
required: true
type: string
permissions:
contents: read
jobs:
stage-release-announcements:
name: Stage Release Announcements
if: ${{ !github.event.repository.fork }}
uses: spring-io/spring-security-release-tools/.github/workflows/release-announcements-stage.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
version: ${{ inputs.version || github.ref_name }}
secrets: inherit
@@ -17,6 +17,7 @@
package org.springframework.security.config.annotation.authentication.configurers.ldap; package org.springframework.security.config.annotation.authentication.configurers.ldap;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.ldap.core.support.BaseLdapPathContextSource;
@@ -590,7 +591,7 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
} }
private int getDefaultPort() { private int getDefaultPort() {
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT, 50, InetAddress.getLoopbackAddress())) {
return serverSocket.getLocalPort(); return serverSocket.getLocalPort();
} }
catch (IOException ex) { catch (IOException ex) {
@@ -32,6 +32,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role; import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
@@ -127,6 +128,11 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, Applicati
this.expressionHandler.setAuthorizationManagerFactory(authorizationManagerFactory); this.expressionHandler.setAuthorizationManagerFactory(authorizationManagerFactory);
} }
@Autowired(required = false)
void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
this.expressionHandler.setPermissionEvaluator(permissionEvaluator);
}
@Autowired(required = false) @Autowired(required = false)
void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults); this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults);
@@ -706,8 +706,10 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
} }
private void configure(H http) { private void configure(H http) {
DPoPAuthenticationProvider authenticationProvider = new DPoPAuthenticationProvider(
getTokenAuthenticationManager(http));
http.authenticationProvider(postProcess(authenticationProvider));
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
http.authenticationProvider(new DPoPAuthenticationProvider(getTokenAuthenticationManager(http)));
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager, AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
getAuthenticationConverter()); getAuthenticationConverter());
authenticationFilter.setRequestMatcher(getRequestMatcher()); authenticationFilter.setRequestMatcher(getRequestMatcher());
@@ -17,6 +17,7 @@
package org.springframework.security.config.ldap; package org.springframework.security.config.ldap;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import org.w3c.dom.Element; import org.w3c.dom.Element;
@@ -188,7 +189,7 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
} }
private String getDefaultPort() { private String getDefaultPort() {
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT, 50, InetAddress.getLoopbackAddress())) {
return String.valueOf(serverSocket.getLocalPort()); return String.valueOf(serverSocket.getLocalPort());
} }
catch (IOException ex) { catch (IOException ex) {
@@ -297,6 +297,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
* @author Ankur Pathak * @author Ankur Pathak
* @author Alexey Nesterov * @author Alexey Nesterov
* @author Yanming Zhou * @author Yanming Zhou
* @author Iain Henderson
* @since 5.0 * @since 5.0
*/ */
public class ServerHttpSecurity { public class ServerHttpSecurity {
@@ -4138,6 +4139,8 @@ public class ServerHttpSecurity {
private ServerAuthenticationFailureHandler authenticationFailureHandler; private ServerAuthenticationFailureHandler authenticationFailureHandler;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler(); private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter(); private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
@@ -4186,6 +4189,20 @@ public class ServerHttpSecurity {
return this; return this;
} }
/**
* Configures the {@link ServerAuthenticationSuccessHandler} to use. The default
* is {@link WebFilterChainServerAuthenticationSuccessHandler}
* @param authenticationSuccessHandler the
* {@link ServerAuthenticationSuccessHandler} to use
* @return the {@link OAuth2ClientSpec} to customize
* @since 7.2
*/
public OAuth2ResourceServerSpec authenticationSuccessHandler(
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
return this;
}
/** /**
* Configures the {@link ServerAuthenticationConverter} to use for requests * Configures the {@link ServerAuthenticationConverter} to use for requests
* authenticating with * authenticating with
@@ -4254,6 +4271,7 @@ public class ServerHttpSecurity {
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver);
oauth2.setServerAuthenticationConverter(this.bearerTokenConverter); oauth2.setServerAuthenticationConverter(this.bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler()); oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
} }
else if (this.jwt != null) { else if (this.jwt != null) {
@@ -4313,6 +4331,13 @@ public class ServerHttpSecurity {
return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint); return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint);
} }
private ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
if (this.authenticationSuccessHandler != null) {
return this.authenticationSuccessHandler;
}
return new WebFilterChainServerAuthenticationSuccessHandler();
}
/** /**
* Configures JWT Resource Server Support * Configures JWT Resource Server Support
*/ */
@@ -4387,6 +4412,7 @@ public class ServerHttpSecurity {
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter); oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler()); oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
} }
@@ -4519,6 +4545,7 @@ public class ServerHttpSecurity {
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter); oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler()); oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
} }
@@ -20,6 +20,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.web.server.ServerAuthenticationEntryPoint import org.springframework.security.web.server.ServerAuthenticationEntryPoint
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.ServerWebExchange
@@ -35,6 +36,8 @@ import org.springframework.web.server.ServerWebExchange
* @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with * @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with
* Bearer Tokens. * Bearer Tokens.
* @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use. * @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use.
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to use after
* authentication success.
*/ */
@ServerSecurityMarker @ServerSecurityMarker
class ServerOAuth2ResourceServerDsl { class ServerOAuth2ResourceServerDsl {
@@ -43,6 +46,7 @@ class ServerOAuth2ResourceServerDsl {
var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
var bearerTokenConverter: ServerAuthenticationConverter? = null var bearerTokenConverter: ServerAuthenticationConverter? = null
var authenticationManagerResolver: ReactiveAuthenticationManagerResolver<ServerWebExchange>? = null var authenticationManagerResolver: ReactiveAuthenticationManagerResolver<ServerWebExchange>? = null
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null
private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null
@@ -115,6 +119,7 @@ class ServerOAuth2ResourceServerDsl {
authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) } authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) }
bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) } bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) }
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) } authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) }
authenticationSuccessHandler?.also { oauth2ResourceServer.authenticationSuccessHandler(authenticationSuccessHandler) }
jwt?.also { oauth2ResourceServer.jwt(jwt) } jwt?.also { oauth2ResourceServer.jwt(jwt) }
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) } opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
} }
@@ -25,12 +25,12 @@ public class MockSecurityContextHolderStrategy implements SecurityContextHolderS
private SecurityContext context; private SecurityContext context;
@Override @Override
public void clearContext() { public synchronized void clearContext() {
this.context = null; this.context = null;
} }
@Override @Override
public SecurityContext getContext() { public synchronized SecurityContext getContext() {
if (this.context == null) { if (this.context == null) {
this.context = createEmptyContext(); this.context = createEmptyContext();
} }
@@ -38,7 +38,7 @@ public class MockSecurityContextHolderStrategy implements SecurityContextHolderS
} }
@Override @Override
public void setContext(SecurityContext context) { public synchronized void setContext(SecurityContext context) {
this.context = context; this.context = context;
} }
@@ -1527,13 +1527,13 @@ public class PrePostMethodSecurityConfigurationTests {
} }
@Configuration
@EnableMethodSecurity @EnableMethodSecurity
static class CustomPermissionEvaluatorConfig { static class CustomPermissionEvaluatorConfig {
@Bean @Bean
MethodSecurityExpressionHandler methodSecurityExpressionHandler() { PermissionEvaluator permissionEvaluator() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); return new PermissionEvaluator() {
expressionHandler.setPermissionEvaluator(new PermissionEvaluator() {
@Override @Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, public boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission) { Object permission) {
@@ -1545,8 +1545,7 @@ public class PrePostMethodSecurityConfigurationTests {
Object permission) { Object permission) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
}); };
return expressionHandler;
} }
} }
@@ -22,6 +22,7 @@ import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey; import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey; import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Base64; import java.util.Base64;
@@ -30,6 +31,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWK;
@@ -47,20 +49,26 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.DPoPProofContext;
import org.springframework.security.oauth2.jwt.DPoPProofJwtDecoderFactory;
import org.springframework.security.oauth2.jwt.DPoPProofReplayValidator;
import org.springframework.security.oauth2.jwt.JwsHeader; import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -247,8 +255,8 @@ public class DPoPAuthenticationTests {
) )
.oauth2ResourceServer((oauth2) -> oauth2 .oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults()) .jwt(Customizer.withDefaults())
.dPoP(Customizer.withDefaults())); .dPoP(Customizer.withDefaults())
.withObjectPostProcessor(dPoPProofVerifierFactoryCustomizer()));
// @formatter:on // @formatter:on
return http.build(); return http.build();
} }
@@ -258,6 +266,25 @@ public class DPoPAuthenticationTests {
return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build(); return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build();
} }
private ObjectPostProcessor<DPoPAuthenticationProvider> dPoPProofVerifierFactoryCustomizer() {
return new ObjectPostProcessor<>() {
@Override
public <O extends DPoPAuthenticationProvider> O postProcess(O authenticationProvider) {
DPoPProofReplayValidator.InMemoryCache inMemoryCache = new DPoPProofReplayValidator.InMemoryCache();
inMemoryCache.setMaxSize(50_000);
inMemoryCache.setMaxRequestsPerKey(500);
DPoPProofReplayValidator dPoPProofReplayValidator = new DPoPProofReplayValidator(inMemoryCache);
dPoPProofReplayValidator.setClockSkew(Duration.ofSeconds(60));
Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = DPoPProofJwtDecoderFactory
.createDefaultJwtValidatorFactory(Collections.singletonList(dPoPProofReplayValidator));
DPoPProofJwtDecoderFactory dPoPProofJwtDecoderFactory = new DPoPProofJwtDecoderFactory();
dPoPProofJwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory);
authenticationProvider.setDPoPProofVerifierFactory(dPoPProofJwtDecoderFactory);
return authenticationProvider;
}
};
}
} }
@RestController @RestController
@@ -73,9 +73,11 @@ import org.springframework.security.oauth2.server.resource.authentication.Reacti
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler; import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -371,6 +373,79 @@ public class OAuth2ResourceServerSpecTests {
verify(handler).onAuthenticationFailure(any(), any()); verify(handler).onAuthenticationFailure(any(), any());
} }
@Test
public void getWhenUsingCustomAuthenticationSuccessHandlerThenUsesIsAccordingly() {
this.spring.register(CustomAuthenticationSuccessHandlerAuthenticationManagerResolverConfig.class).autowire();
ServerAuthenticationSuccessHandler handler = this.spring.getContext()
.getBean(ServerAuthenticationSuccessHandler.class);
ReactiveAuthenticationManager authenticationManager = this.spring.getContext()
.getBean(ReactiveAuthenticationManager.class);
given(authenticationManager.authenticate(any()))
.willAnswer((input) -> Mono.just(input.getArgument(0, Authentication.class)));
given(handler.onAuthenticationSuccess(any(), any())).willAnswer((input) -> {
WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class);
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
});
// @formatter:off
this.client.get()
.headers((headers) -> headers.setBearerAuth(this.messageReadToken))
.exchange()
.expectStatus().isUnauthorized();
// @formatter:on
verify(handler).onAuthenticationSuccess(any(), any());
}
@Test
public void getWhenUsingCustomAuthenticationSuccessHandlerWithJwtThenUsesIsAccordingly() {
this.spring.register(CustomAuthenticationSuccessHandlerJwtConfig.class).autowire();
ServerAuthenticationSuccessHandler handler = this.spring.getContext()
.getBean(ServerAuthenticationSuccessHandler.class);
ReactiveAuthenticationManager authenticationManager = this.spring.getContext()
.getBean(ReactiveAuthenticationManager.class);
given(authenticationManager.authenticate(any()))
.willAnswer((input) -> Mono.just(input.getArgument(0, Authentication.class)));
given(handler.onAuthenticationSuccess(any(), any())).willAnswer((input) -> {
WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class);
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
});
// @formatter:off
this.client.get()
.headers((headers) -> headers.setBearerAuth(this.messageReadToken))
.exchange()
.expectStatus().isUnauthorized();
// @formatter:on
verify(handler).onAuthenticationSuccess(any(), any());
}
@Test
public void getWhenUsingCustomAuthenticationSuccessHandlerWIthOpaqueTokenThenUsesIsAccordingly() {
this.spring.register(CustomAuthenticationSuccessHandlerOpaqueTokenConfig.class, RootController.class)
.autowire();
this.spring.getContext()
.getBean(MockWebServer.class)
.setDispatcher(requiresAuth(this.clientId, this.clientSecret, this.active));
ServerAuthenticationSuccessHandler handler = this.spring.getContext()
.getBean(ServerAuthenticationSuccessHandler.class);
ReactiveAuthenticationManager authenticationManager = this.spring.getContext()
.getBean(ReactiveAuthenticationManager.class);
given(authenticationManager.authenticate(any()))
.willAnswer((input) -> Mono.just(input.getArgument(0, Authentication.class)));
given(handler.onAuthenticationSuccess(any(), any())).willAnswer((input) -> {
WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class);
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
});
// @formatter:off
this.client.get()
.headers((headers) -> headers
.setBearerAuth(this.messageReadToken)
)
.exchange()
.expectStatus().isOk();
// @formatter:on
verify(handler).onAuthenticationSuccess(any(), any());
}
@Test @Test
public void postWhenSignedThenReturnsOk() { public void postWhenSignedThenReturnsOk() {
this.spring.register(PublicKeyConfig.class, RootController.class).autowire(); this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
@@ -950,6 +1025,111 @@ public class OAuth2ResourceServerSpecTests {
} }
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
static class CustomAuthenticationSuccessHandlerAuthenticationManagerResolverConfig {
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.authenticationSuccessHandler(authenticationSuccessHandler())
.authenticationManagerResolver((exchange) -> Mono.just(authenticationManager()))
);
// @formatter:on
return http.build();
}
@Bean
ReactiveAuthenticationManager authenticationManager() {
return mock(ReactiveAuthenticationManager.class);
}
@Bean
ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
return mock(ServerAuthenticationSuccessHandler.class);
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
static class CustomAuthenticationSuccessHandlerJwtConfig {
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.authenticationSuccessHandler(authenticationSuccessHandler())
.jwt((jwt) -> jwt.authenticationManager(authenticationManager()))
);
// @formatter:on
return http.build();
}
@Bean
ReactiveAuthenticationManager authenticationManager() {
return mock(ReactiveAuthenticationManager.class);
}
@Bean
ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
return mock(ServerAuthenticationSuccessHandler.class);
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
static class CustomAuthenticationSuccessHandlerOpaqueTokenConfig {
private MockWebServer mockWebServer = new MockWebServer();
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
String introspectionUri = mockWebServer().url("/introspect").toString();
// @formatter:off
http
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.authenticationSuccessHandler(authenticationSuccessHandler())
.opaqueToken((opaqueToken) -> opaqueToken
.introspectionUri(introspectionUri)
.introspectionClientCredentials("client", "secret"))
);
// @formatter:on
return http.build();
}
@Bean
ReactiveAuthenticationManager authenticationManager() {
return mock(ReactiveAuthenticationManager.class);
}
@Bean
ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
return mock(ServerAuthenticationSuccessHandler.class);
}
@Bean
MockWebServer mockWebServer() {
return this.mockWebServer;
}
@PreDestroy
void shutdown() throws IOException {
this.mockWebServer.shutdown();
}
}
@EnableWebFlux @EnableWebFlux
@EnableWebFluxSecurity @EnableWebFluxSecurity
static class CustomBearerTokenServerAuthenticationConverter { static class CustomBearerTokenServerAuthenticationConverter {
@@ -17,6 +17,7 @@
package org.springframework.security.config.web.server package org.springframework.security.config.web.server
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject import io.mockk.mockkObject
import io.mockk.verify import io.mockk.verify
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -37,6 +38,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.WebFilterExchange import org.springframework.security.web.server.WebFilterExchange
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.config.EnableWebFlux
@@ -183,6 +185,46 @@ class ServerOAuth2ResourceServerDslTests {
} }
@Test
fun `request when custom authentication success handler then success handler used`() {
this.spring.register(AuthenticationSuccessHandlerConfig::class.java).autowire()
every {
AuthenticationSuccessHandlerConfig.SUCCESS_HANDLER.onAuthenticationSuccess(any(), any())
} returns Mono.empty()
this.client.get()
.uri("/")
.headers { it.setBearerAuth(validJwt) }
.exchange()
verify(exactly = 1) { AuthenticationSuccessHandlerConfig.SUCCESS_HANDLER.onAuthenticationSuccess(any(), any()) }
}
@Configuration
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationSuccessHandlerConfig {
companion object {
val SUCCESS_HANDLER: ServerAuthenticationSuccessHandler = mockk()
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
authenticationSuccessHandler = SUCCESS_HANDLER
jwt {
publicKey = publicKey()
}
}
}
}
}
@Test @Test
fun `request when custom bearer token converter configured then custom converter used`() { fun `request when custom bearer token converter configured then custom converter used`() {
this.spring.register(BearerTokenConverterConfig::class.java).autowire() this.spring.register(BearerTokenConverterConfig::class.java).autowire()
@@ -34,7 +34,6 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/** /**
* Iterates an {@link Authentication} request through a list of * Iterates an {@link Authentication} request through a list of
@@ -138,8 +137,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
private void checkState() { private void checkState() {
Assert.isTrue(this.parent != null || !this.providers.isEmpty(), Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
"A parent AuthenticationManager or a list of AuthenticationProviders is required"); "A parent AuthenticationManager or a list of AuthenticationProviders is required");
Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null), Assert.noNullElements(this.providers, "providers list cannot contain null values");
"providers list cannot contain null values");
} }
/** /**
@@ -123,7 +123,7 @@ public final class PostFilterAuthorizationMethodInterceptor implements Authoriza
/** /**
* Filter a {@code returnedObject} using the {@link PostFilter} annotation that the * Filter a {@code returnedObject} using the {@link PostFilter} annotation that the
* {@link MethodInvocation} specifies. * {@link MethodInvocation} specifies.
* @param mi the {@link MethodInvocation} to check check * @param mi the {@link MethodInvocation} to check
* @return filtered {@code returnedObject} * @return filtered {@code returnedObject}
*/ */
@Override @Override
@@ -131,7 +131,7 @@ public final class DelegatingSecurityContextCallable<V> implements Callable<V> {
/** /**
* Creates a {@link DelegatingSecurityContextCallable} and with the given * Creates a {@link DelegatingSecurityContextCallable} and with the given
* {@link Callable} and {@link SecurityContext}, but if the securityContext is null * {@link Callable} and {@link SecurityContext}, but if the securityContext is null
* will defaults to the current {@link SecurityContext} on the * will default to the current {@link SecurityContext} on the
* {@link SecurityContextHolder} * {@link SecurityContextHolder}
* @param delegate the delegate {@link DelegatingSecurityContextCallable} to run with * @param delegate the delegate {@link DelegatingSecurityContextCallable} to run with
* the specified {@link SecurityContext}. Cannot be null. * the specified {@link SecurityContext}. Cannot be null.
@@ -47,7 +47,7 @@ public final class DelegatingSecurityContextRunnable implements Runnable {
.getContextHolderStrategy(); .getContextHolderStrategy();
/** /**
* The {@link SecurityContext} that the delegate {@link Runnable} will be ran as. * The {@link SecurityContext} that the delegate {@link Runnable} will be run as.
*/ */
private SecurityContext delegateSecurityContext; private SecurityContext delegateSecurityContext;
@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath. * {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath.
* *
* @author Steve Riesenberg * @author Steve Riesenberg
* @author Tadaya Tsuyukubo
* @since 6.5 * @since 6.5
* @see io.micrometer.context.ContextRegistry * @see io.micrometer.context.ContextRegistry
*/ */
@@ -53,7 +54,9 @@ public final class SecurityContextHolderThreadLocalAccessor implements ThreadLoc
@Override @Override
public void setValue(SecurityContext securityContext) { public void setValue(SecurityContext securityContext) {
Assert.notNull(securityContext, "securityContext cannot be null"); Assert.notNull(securityContext, "securityContext cannot be null");
SecurityContextHolder.setContext(securityContext); SecurityContext newContext = SecurityContextHolder.createEmptyContext();
newContext.setAuthentication(securityContext.getAuthentication());
SecurityContextHolder.setContext(newContext);
} }
@Override @Override
@@ -132,7 +132,7 @@ public class KeyBasedPersistenceTokenService implements TokenService, Initializi
// Verification // Verification
String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString(); String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString();
String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret); String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure"); Assert.isTrue(Utf8.isEqual(expectedSha512Hex, sha1Hex), "Key verification failure");
return new DefaultToken(key, creationTime, extendedInfo.toString()); return new DefaultToken(key, creationTime, extendedInfo.toString());
} }
@@ -35,7 +35,7 @@ import org.jspecify.annotations.Nullable;
* building block for more sophisticated token-based solutions. For example, * building block for more sophisticated token-based solutions. For example,
* authentication systems that depend on stateless session keys. These could, for * authentication systems that depend on stateless session keys. These could, for
* instance, place the username inside the user-specified extended information associated * instance, place the username inside the user-specified extended information associated
* with the key). It is important to recognise that we do not intend for this interface to * with the key. It is important to recognise that we do not intend for this interface to
* be expanded to provide such capabilities directly. * be expanded to provide such capabilities directly.
* </p> * </p>
* *
@@ -323,7 +323,7 @@ public class User implements UserDetails, CredentialsContainer {
/** /**
* Builds the user to be added. At minimum the username, password, and authorities * Builds the user to be added. At minimum the username, password, and authorities
* should provided. The remaining attributes have reasonable defaults. * should be provided. The remaining attributes have reasonable defaults.
*/ */
public static final class UserBuilder { public static final class UserBuilder {
@@ -38,7 +38,7 @@ public interface UserDetailsService {
* may possibly be case sensitive, or case insensitive depending on how the * may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code> * implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what * object that comes back may have a username that is of a different case than what
* was actually requested.. * was actually requested.
* @param username the username identifying the user whose data is required. * @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>) * @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no * @throws UsernameNotFoundException if the user could not be found or the user has no
@@ -16,7 +16,7 @@
/** /**
* Contains simple user and authority group account provisioning interfaces together with * Contains simple user and authority group account provisioning interfaces together with
* a a JDBC-based implementation. * a JDBC-based implementation.
*/ */
@NullMarked @NullMarked
package org.springframework.security.provisioning; package org.springframework.security.provisioning;
@@ -259,7 +259,9 @@ public final class InetAddressMatchers {
if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) { if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) {
return true; return true;
} }
if (address.isAnyLocalAddress()) {
return true;
}
byte[] rawAddress = address.getAddress(); byte[] rawAddress = address.getAddress();
if (rawAddress.length == 16) { if (rawAddress.length == 16) {
@@ -335,6 +337,9 @@ public final class InetAddressMatchers {
@Override @Override
public boolean matches(@Nullable InetAddress address) { public boolean matches(@Nullable InetAddress address) {
if (address == null) {
return false;
}
return !this.internalMatcher.matches(address); return !this.internalMatcher.matches(address);
} }
@@ -16,11 +16,18 @@
package org.springframework.security.core.context; package org.springframework.security.core.context;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -29,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link SecurityContextHolderThreadLocalAccessor}. * Tests for {@link SecurityContextHolderThreadLocalAccessor}.
* *
* @author Steve Riesenberg * @author Steve Riesenberg
* @author Tadaya Tsuyukubo
*/ */
public class SecurityContextHolderThreadLocalAccessorTests { public class SecurityContextHolderThreadLocalAccessorTests {
@@ -65,9 +73,11 @@ public class SecurityContextHolderThreadLocalAccessorTests {
@Test @Test
public void setValueWhenSecurityContextThenSetsSecurityContextHolder() { public void setValueWhenSecurityContextThenSetsSecurityContextHolder() {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); Authentication authentication = new TestingAuthenticationToken("user", "password");
securityContext.setAuthentication(authentication);
this.threadLocalAccessor.setValue(securityContext); this.threadLocalAccessor.setValue(securityContext);
assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext); assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(authentication);
} }
@Test @Test
@@ -90,4 +100,41 @@ public class SecurityContextHolderThreadLocalAccessorTests {
assertThat(SecurityContextHolder.getContext()).isEqualTo(emptyContext); assertThat(SecurityContextHolder.getContext()).isEqualTo(emptyContext);
} }
@Test
public void newSecurityContextInDifferentThread() throws Exception {
Authentication authA = new TestingAuthenticationToken("foo", "password");
Authentication authB = new TestingAuthenticationToken("bar", "password");
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authA);
SecurityContextHolder.setContext(securityContext);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<SecurityContext> contextHolder = new AtomicReference<>();
AtomicReference<Authentication> authHolder = new AtomicReference<>();
Runnable runnable = () -> {
SecurityContext context = SecurityContextHolder.getContext();
contextHolder.set(context);
authHolder.set(context.getAuthentication());
context.setAuthentication(authB);
latch.countDown();
};
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
executor.afterPropertiesSet();
executor.execute(runnable);
boolean finished = latch.await(10, TimeUnit.SECONDS);
assertThat(finished).isTrue();
assertThat(contextHolder.get()).isNotSameAs(securityContext);
assertThat(authHolder.get()).isSameAs(authA);
SecurityContext current = SecurityContextHolder.getContext();
assertThat(current).isSameAs(securityContext);
assertThat(current.getAuthentication()).isSameAs(authA);
}
} }
@@ -276,16 +276,17 @@ class InetAddressMatchersTests {
} }
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = { "127.0.0.1", "127.0.0.255" }) @ValueSource(strings = { "127.0.0.1", "127.0.0.255", "0.0.0.0" })
void matchesWhenIpv4LoopbackThenReturnsTrue(String address) throws Exception { void matchesWhenIpv4LoopbackThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build(); InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue(); assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
} }
@Test @ParameterizedTest
void matchesWhenIpv6LoopbackThenReturnsTrue() throws Exception { @ValueSource(strings = { "::1", "::" })
void matchesWhenIpv6LoopbackThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build(); InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("::1"))).isTrue(); assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
} }
@ParameterizedTest @ParameterizedTest
@@ -415,6 +416,12 @@ class InetAddressMatchersTests {
@Nested @Nested
class ExternalInetAddressMatcherTests { class ExternalInetAddressMatcherTests {
@Test
void matchesWhenInetAddressNullThenReturnsFalse() {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches((InetAddress) null)).isFalse();
}
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" }) @ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
void matchesWhenIpv4PublicThenReturnsTrue(String address) throws Exception { void matchesWhenIpv4PublicThenReturnsTrue(String address) throws Exception {
@@ -21,6 +21,9 @@ import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException; import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.jspecify.annotations.Nullable;
/** /**
* UTF-8 Charset encoder/decoder. * UTF-8 Charset encoder/decoder.
@@ -66,4 +69,21 @@ public final class Utf8 {
} }
} }
/**
* Constant time comparison to prevent against timing attacks.
* @param expected the expected {@link CharSequence}
* @param actual the actual {@link CharSequence}
* @return true if {@code expected} and {@code actual} are equal, false otherwise
* @since 5.7.26
*/
public static boolean isEqual(@Nullable CharSequence expected, @Nullable CharSequence actual) {
byte[] expectedBytes = bytesUtf8(expected);
byte[] actualBytes = bytesUtf8(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
}
private static byte @Nullable [] bytesUtf8(@Nullable CharSequence s) {
return (s != null) ? Utf8.encode(s) : null;
}
} }
@@ -37,7 +37,9 @@ import org.springframework.security.crypto.util.EncodingUtils;
* *
* @author Keith Donald * @author Keith Donald
* @author Dave Syer * @author Dave Syer
* @deprecated Use {@link AesCbcBytesEncryptor} or {@link AesGcmBytesEncryptor} instead.
*/ */
@Deprecated
public final class AesBytesEncryptor implements BytesEncryptor { public final class AesBytesEncryptor implements BytesEncryptor {
private final SecretKey secretKey; private final SecretKey secretKey;
@@ -0,0 +1,165 @@
/*
* 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.crypto.encrypt;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.util.EncodingUtils;
/**
* {@link BytesEncryptor} that uses 256-bit AES/CBC/PKCS5Padding with a random 16-byte
* initialization vector. The IV is prepended to the ciphertext on encrypt and stripped on
* decrypt.
*
* <p>
* Note that CBC mode provides confidentiality but <em>not</em> integrity or authenticity.
* Applications that require authenticated encryption should prefer
* {@link AesGcmBytesEncryptor}. See the <a href=
* "https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html">
* OWASP Cryptographic Storage Cheat Sheet</a> for guidance on choosing a cipher mode.
*
* <p>
* When key derivation is used via {@link #withPassword(String, CharSequence)}, the key is
* derived using PBKDF2WithHmacSHA256 with {@code DEFAULT_PBKDF2_ITERATIONS} iterations
* per the <a href=
* "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">
* OWASP Password Storage Cheat Sheet</a>. Because derivation is intentionally expensive,
* the encryptor instance should be created once and reused rather than constructed
* per-operation.
*
* @author Josh Cummings
* @since 5.7.26
* @see AesGcmBytesEncryptor
* @see AesBytesEncryptor
*/
public final class AesCbcBytesEncryptor implements BytesEncryptor {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private static final int IV_LENGTH_BYTES = 16;
private static final int DEFAULT_PBKDF2_ITERATIONS = 600_000;
private final SecretKey secretKey;
private final Cipher encryptor;
private final Cipher decryptor;
private final BytesKeyGenerator ivGenerator;
private AesCbcBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator) {
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
this.encryptor = CipherUtils.newCipher(ALGORITHM);
this.decryptor = CipherUtils.newCipher(ALGORITHM);
this.ivGenerator = ivGenerator;
}
/**
* Creates an encryptor that derives its key from the given password and hex-encoded
* salt using PBKDF2WithHmacSHA1.
* @param password the password value
* @param salt the hex-encoded salt value
*/
public static Builder withPassword(String password, CharSequence salt) {
return new Builder(password, salt);
}
/**
* Creates an encryptor using the supplied {@link SecretKey}.
* @param secretKey the secret (symmetric) key
*/
public static Builder withSecretKey(SecretKey secretKey) {
return new Builder(secretKey);
}
@Override
public byte[] encrypt(byte[] bytes) {
synchronized (this.encryptor) {
byte[] iv = this.ivGenerator.generateKey();
CipherUtils.initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey, new IvParameterSpec(iv));
byte[] ciphertext = CipherUtils.doFinal(this.encryptor, bytes);
return EncodingUtils.concatenate(iv, ciphertext);
}
}
@Override
public byte[] decrypt(byte[] encryptedBytes) {
int ivLength = this.ivGenerator.getKeyLength();
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, ivLength);
byte[] ciphertext = EncodingUtils.subArray(encryptedBytes, ivLength, encryptedBytes.length);
synchronized (this.decryptor) {
CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey, new IvParameterSpec(iv));
return CipherUtils.doFinal(this.decryptor, ciphertext);
}
}
private static SecretKey deriveKey(String password, CharSequence salt) {
return CipherUtils.newSecretKey("PBKDF2WithHmacSHA256",
new PBEKeySpec(password.toCharArray(), Hex.decode(salt), DEFAULT_PBKDF2_ITERATIONS, 256));
}
/**
* A Builder for {@link AesCbcBytesEncryptor}.
*/
public static final class Builder {
private final SecretKey secretKey;
private BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(IV_LENGTH_BYTES);
private Builder(SecretKey secretKey) {
this.secretKey = secretKey;
}
private Builder(String password, CharSequence salt) {
this.secretKey = deriveKey(password, salt);
}
/**
* Sets the {@link BytesKeyGenerator} to use for generating the initialization
* vector.
* @param ivGenerator the {@link BytesKeyGenerator} to use for generating the
* initialization vector
* @return this builder
*/
public Builder ivGenerator(BytesKeyGenerator ivGenerator) {
Objects.requireNonNull(ivGenerator, "ivGenerator cannot be null");
this.ivGenerator = ivGenerator;
return this;
}
/**
* Builds the {@link AesCbcBytesEncryptor}.
* @return the {@link AesCbcBytesEncryptor}
*/
public AesCbcBytesEncryptor build() {
return new AesCbcBytesEncryptor(this.secretKey, this.ivGenerator);
}
}
}
@@ -0,0 +1,172 @@
/*
* 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.crypto.encrypt;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.util.EncodingUtils;
/**
* {@link BytesEncryptor} that uses 256-bit AES/GCM/NoPadding with a random 16-byte
* initialization vector and a 128-bit authentication tag. The IV is prepended to the
* ciphertext on encrypt and stripped on decrypt. GCM provides authenticated encryption
* (AEAD): both confidentiality and integrity are protected, and decryption throws if the
* ciphertext has been tampered with.
*
* <p>
* This class uses a 16-byte (128-bit) IV rather than the 12-byte (96-bit) IV recommended
* by NIST SP 800-38D for GCM. Both lengths are cryptographically valid; the 16-byte
* choice maintains consistency with the rest of the Spring Security crypto module. For
* additional guidance, please see the <a href=
* "https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html">
* OWASP Cryptographic Storage Cheat Sheet</a>.
*
* <p>
* When key derivation is used via {@link #withPassword(String, CharSequence)}, the key is
* derived using PBKDF2WithHmacSHA256 with {@code DEFAULT_PBKDF2_ITERATIONS} iterations
* per the <a href=
* "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">
* OWASP Password Storage Cheat Sheet</a>. Because derivation is intentionally expensive,
* the encryptor instance should be created once and reused rather than constructed
* per-operation.
*
* @author Josh Cummings
* @since 5.7.26
* @see AesCbcBytesEncryptor
* @see AesBytesEncryptor
*/
public final class AesGcmBytesEncryptor implements BytesEncryptor {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH_BYTES = 16;
private static final int TAG_LENGTH_BITS = 128;
private static final int DEFAULT_PBKDF2_ITERATIONS = 600_000;
private final SecretKey secretKey;
private final Cipher encryptor;
private final Cipher decryptor;
private final BytesKeyGenerator ivGenerator;
private AesGcmBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator) {
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
this.encryptor = CipherUtils.newCipher(ALGORITHM);
this.decryptor = CipherUtils.newCipher(ALGORITHM);
this.ivGenerator = ivGenerator;
}
/**
* Creates an encryptor that derives its key from the given password and hex-encoded
* salt using PBKDF2WithHmacSHA1.
* @param password the password value
* @param salt the hex-encoded salt value
*/
public static Builder withPassword(String password, CharSequence salt) {
return new Builder(password, salt);
}
/**
* Creates an encryptor using the supplied {@link SecretKey}.
* @param secretKey the secret (symmetric) key
*/
public static Builder withSecretKey(SecretKey secretKey) {
return new Builder(secretKey);
}
@Override
public byte[] encrypt(byte[] bytes) {
synchronized (this.encryptor) {
byte[] iv = this.ivGenerator.generateKey();
CipherUtils.initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey,
new GCMParameterSpec(TAG_LENGTH_BITS, iv));
byte[] ciphertext = CipherUtils.doFinal(this.encryptor, bytes);
return EncodingUtils.concatenate(iv, ciphertext);
}
}
@Override
public byte[] decrypt(byte[] encryptedBytes) {
int ivLength = this.ivGenerator.getKeyLength();
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, ivLength);
byte[] ciphertext = EncodingUtils.subArray(encryptedBytes, ivLength, encryptedBytes.length);
synchronized (this.decryptor) {
CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey,
new GCMParameterSpec(TAG_LENGTH_BITS, iv));
return CipherUtils.doFinal(this.decryptor, ciphertext);
}
}
private static SecretKey deriveKey(String password, CharSequence salt) {
return CipherUtils.newSecretKey("PBKDF2WithHmacSHA256",
new PBEKeySpec(password.toCharArray(), Hex.decode(salt), DEFAULT_PBKDF2_ITERATIONS, 256));
}
/**
* A Builder for {@link AesGcmBytesEncryptor}.
*/
public static final class Builder {
private final SecretKey secretKey;
private BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(IV_LENGTH_BYTES);
private Builder(SecretKey secretKey) {
this.secretKey = secretKey;
}
private Builder(String password, CharSequence salt) {
this.secretKey = deriveKey(password, salt);
}
/**
* Sets the {@link BytesKeyGenerator} to use for generating the initialization
* vector.
* @param ivGenerator the {@link BytesKeyGenerator} to use for generating the
* initialization vector
* @return this builder
*/
public Builder ivGenerator(BytesKeyGenerator ivGenerator) {
Objects.requireNonNull(ivGenerator, "ivGenerator cannot be null");
this.ivGenerator = ivGenerator;
return this;
}
/**
* Builds the {@link AesGcmBytesEncryptor}.
* @return the {@link AesGcmBytesEncryptor}
*/
public AesGcmBytesEncryptor build() {
return new AesGcmBytesEncryptor(this.secretKey, this.ivGenerator);
}
}
}
@@ -16,7 +16,6 @@
package org.springframework.security.crypto.encrypt; package org.springframework.security.crypto.encrypt;
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.keygen.KeyGenerators;
/** /**
@@ -42,9 +41,14 @@ public final class Encryptors {
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* key * key
* @deprecated Use {@link AesGcmBytesEncryptor#withPassword(String, CharSequence)}
* instead.
*/ */
@Deprecated
@SuppressWarnings("deprecation")
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) { public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16), CipherAlgorithm.GCM); return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16),
AesBytesEncryptor.CipherAlgorithm.GCM);
} }
/** /**
@@ -53,18 +57,16 @@ public final class Encryptors {
* Function #2). Salts the password to prevent dictionary attacks against the key. The * Function #2). Salts the password to prevent dictionary attacks against the key. The
* provided salt is expected to be hex-encoded; it should be random and at least 8 * provided salt is expected to be hex-encoded; it should be random and at least 8
* bytes in length. Also applies a random 16-byte initialization vector to ensure each * bytes in length. Also applies a random 16-byte initialization vector to ensure each
* encrypted message will be unique. Requires Java 6. NOTE: This mode is not * encrypted message will be unique. Requires Java 6.
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
* and does not provide any guarantees about the authenticity of the data. For a more
* secure alternative, users should prefer
* {@link #stronger(CharSequence, CharSequence)}.
* @param password the password used to generate the encryptor's secret key; should * @param password the password used to generate the encryptor's secret key; should
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* key * key
* * @deprecated Use {@link AesCbcBytesEncryptor#withPassword(String, CharSequence)}
* @see Encryptors#stronger(CharSequence, CharSequence) * instead.
*/ */
@Deprecated
@SuppressWarnings("deprecation")
public static BytesEncryptor standard(CharSequence password, CharSequence salt) { public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16)); return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16));
} }
@@ -74,8 +76,12 @@ public final class Encryptors {
* text is hex-encoded. * text is hex-encoded.
* @param password the password used to generate the encryptor's secret key; should * @param password the password used to generate the encryptor's secret key; should
* not be shared * not be shared
* @deprecated Use {@link AesGcmBytesEncryptor#withPassword(String, CharSequence)}
* instead.
* @see Encryptors#stronger(CharSequence, CharSequence) * @see Encryptors#stronger(CharSequence, CharSequence)
*/ */
@Deprecated
@SuppressWarnings("deprecation")
public static TextEncryptor delux(CharSequence password, CharSequence salt) { public static TextEncryptor delux(CharSequence password, CharSequence salt) {
return new HexEncodingTextEncryptor(stronger(password, salt)); return new HexEncodingTextEncryptor(stronger(password, salt));
} }
@@ -85,8 +91,12 @@ public final class Encryptors {
* text is hex-encoded. * text is hex-encoded.
* @param password the password used to generate the encryptor's secret key; should * @param password the password used to generate the encryptor's secret key; should
* not be shared * not be shared
* @deprecated Use {@link AesCbcBytesEncryptor#withPassword(String, CharSequence)}
* instead.
* @see Encryptors#standard(CharSequence, CharSequence) * @see Encryptors#standard(CharSequence, CharSequence)
*/ */
@Deprecated
@SuppressWarnings("deprecation")
public static TextEncryptor text(CharSequence password, CharSequence salt) { public static TextEncryptor text(CharSequence password, CharSequence salt) {
return new HexEncodingTextEncryptor(standard(password, salt)); return new HexEncodingTextEncryptor(standard(password, salt));
} }
@@ -16,8 +16,6 @@
package org.springframework.security.crypto.password; package org.springframework.security.crypto.password;
import java.security.MessageDigest;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.security.crypto.codec.Utf8; import org.springframework.security.crypto.codec.Utf8;
@@ -39,15 +37,7 @@ final class PasswordEncoderUtils {
* @return * @return
*/ */
static boolean equals(String expected, @Nullable String actual) { static boolean equals(String expected, @Nullable String actual) {
byte[] expectedBytes = bytesUtf8(expected); return Utf8.isEqual(expected, actual);
byte[] actualBytes = bytesUtf8(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
}
private static byte @Nullable [] bytesUtf8(@Nullable String s) {
// need to check if Utf8.encode() runs in constant time (probably not).
// This may leak length of string.
return (s != null) ? Utf8.encode(s) : null;
} }
} }
@@ -24,6 +24,7 @@ import com.password4j.BalloonHashingFunction;
import com.password4j.Hash; import com.password4j.Hash;
import com.password4j.Password; import com.password4j.Password;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder; import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -140,7 +141,7 @@ public class BalloonHashingPassword4jPasswordEncoder extends AbstractValidatingP
String expectedHash = parts[1]; String expectedHash = parts[1];
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction); Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction);
return expectedHash.equals(hash.getResult()); return Utf8.isEqual(expectedHash, hash.getResult());
} }
catch (IllegalArgumentException ex) { catch (IllegalArgumentException ex) {
// Invalid Base64 encoding // Invalid Base64 encoding
@@ -24,6 +24,7 @@ import com.password4j.Hash;
import com.password4j.PBKDF2Function; import com.password4j.PBKDF2Function;
import com.password4j.Password; import com.password4j.Password;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder; import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -138,7 +139,7 @@ public class Pbkdf2Password4jPasswordEncoder extends AbstractValidatingPasswordE
String expectedHash = parts[1]; String expectedHash = parts[1];
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function); Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function);
return expectedHash.equals(hash.getResult()); return Utf8.isEqual(expectedHash, hash.getResult());
} }
catch (IllegalArgumentException ex) { catch (IllegalArgumentException ex) {
// Invalid Base64 encoding // Invalid Base64 encoding
@@ -37,4 +37,48 @@ public class Utf8Tests {
assertThat(decoded).isEqualTo("6048b75ed560785c"); assertThat(decoded).isEqualTo("6048b75ed560785c");
} }
@Test
public void isEqualWhenDifferentLengthThenFalse() {
assertThat(Utf8.isEqual("abc", "a")).isFalse();
assertThat(Utf8.isEqual("a", "abc")).isFalse();
}
@Test
public void isEqualWhenNullAndNotEmptyThenFalse() {
assertThat(Utf8.isEqual(null, "a")).isFalse();
assertThat(Utf8.isEqual("a", null)).isFalse();
}
@Test
public void isEqualWhenNullAndNullThenTrue() {
assertThat(Utf8.isEqual(null, null)).isTrue();
}
@Test
public void isEqualWhenNullAndEmptyThenFalse() {
assertThat(Utf8.isEqual(null, "")).isFalse();
assertThat(Utf8.isEqual("", null)).isFalse();
}
@Test
public void isEqualWhenNotEmptyAndEmptyThenFalse() {
assertThat(Utf8.isEqual("abc", "")).isFalse();
assertThat(Utf8.isEqual("", "abc")).isFalse();
}
@Test
public void isEqualWhenEmptyAndEmptyThenTrue() {
assertThat(Utf8.isEqual("", "")).isTrue();
}
@Test
public void isEqualWhenDifferentCaseThenFalse() {
assertThat(Utf8.isEqual("aBc", "abc")).isFalse();
}
@Test
public void isEqualWhenSameThenTrue() {
assertThat(Utf8.isEqual("abcdef", "abcdef")).isTrue();
}
} }
@@ -0,0 +1,203 @@
/*
* 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.crypto.encrypt;
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import javax.crypto.spec.PBEKeySpec;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm;
import org.springframework.security.crypto.util.EncodingUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AesCbcBytesEncryptor}.
*/
class AesCbcBytesEncryptorTests {
private final String secret = "value";
private final String password = "password";
private final String hexSalt = "deadbeef";
@Test
void roundtripWhenUsingPasswordAndSaltThenEncryptsAndDecrypts() {
CryptoAssumptions.assumeCBCJCE();
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() {
CryptoAssumptions.assumeCBCJCE();
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
SecretKey secretKey = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(secretKey).build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void encryptWhenUsingMockIvThenProducesKnownCiphertext() {
CryptoAssumptions.assumeCBCJCE();
BytesKeyGenerator mockGenerator = mock(BytesKeyGenerator.class);
given(mockGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
given(mockGenerator.getKeyLength()).willReturn(16);
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt)
.ivGenerator(mockGenerator)
.build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(Hex.encode(encrypted)).isEqualTo("4b0febebd439db7ca77153cb254520c3b7232ac29355d07869433f1ecf55fe94");
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void encryptProducesUniqueOutputAndIvIsPrePended() {
CryptoAssumptions.assumeCBCJCE();
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] first = encryptor.encrypt(this.secret.getBytes());
byte[] second = encryptor.encrypt(this.secret.getBytes());
assertThat(first).isNotEqualTo(second);
assertThat(first.length).isGreaterThan(16);
}
@Test
@SuppressWarnings("deprecation")
void migratesFromDeprecatedNullIvCbcToAesCbcBytesEncryptor() {
CryptoAssumptions.assumeCBCJCE();
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt);
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
AesCbcBytesEncryptor modern = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
BytesEncryptor migrating = new MigratingBytesEncryptor("{CBC}", modern, deprecated);
assertThat(new String(migrating.decrypt(encrypted))).isEqualTo(this.secret);
byte[] migrated = migrating.encrypt(this.secret.getBytes());
assertThat(migrated[0]).isEqualTo((byte) '{');
assertThat(migrated[1]).isEqualTo((byte) 'C');
assertThat(migrated[2]).isEqualTo((byte) 'B');
assertThat(migrated[3]).isEqualTo((byte) 'C');
assertThat(migrated[4]).isEqualTo((byte) '}');
assertThat(new String(migrating.decrypt(migrated))).isEqualTo(this.secret);
}
@Test
@SuppressWarnings("deprecation")
void withSecretWhenAesBytesEncryptorEncryptsThenDecrypts() {
CryptoAssumptions.assumeCBCJCE();
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
AesBytesEncryptor.CipherAlgorithm.CBC);
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(key).build();
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
@SuppressWarnings("deprecation")
void aesBytesEncryptorWhenEncryptsThenAesCbcBytesEncryptorDecrypts() {
CryptoAssumptions.assumeCBCJCE();
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(key).build();
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
AesBytesEncryptor.CipherAlgorithm.CBC);
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(deprecated.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void roundtripWhenUsingCustomIvGeneratorThenEncryptsAndDecrypts() {
CryptoAssumptions.assumeCBCJCE();
BytesKeyGenerator customIvGenerator = mock(BytesKeyGenerator.class);
given(customIvGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
given(customIvGenerator.getKeyLength()).willReturn(16);
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt)
.ivGenerator(customIvGenerator)
.build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
@SuppressWarnings("deprecation")
void withPasswordDerivesADifferentKeyThanAesBytesEncryptor() {
CryptoAssumptions.assumeCBCJCE();
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt,
KeyGenerators.secureRandom(16));
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
}
private static final class MigratingBytesEncryptor implements BytesEncryptor {
private final byte[] prefix;
private final BytesEncryptor write;
private final BytesEncryptor deprecated;
MigratingBytesEncryptor(String prefix, BytesEncryptor write, BytesEncryptor deprecated) {
this.prefix = prefix.getBytes(StandardCharsets.US_ASCII);
this.write = write;
this.deprecated = deprecated;
}
@Override
public byte[] encrypt(byte[] bytes) {
return EncodingUtils.concatenate(this.prefix, this.write.encrypt(bytes));
}
@Override
public byte[] decrypt(byte[] encryptedBytes) {
if (startsWith(encryptedBytes, this.prefix)) {
byte[] bytes = EncodingUtils.subArray(encryptedBytes, this.prefix.length, encryptedBytes.length);
return this.write.decrypt(bytes);
}
return this.deprecated.decrypt(encryptedBytes);
}
private static boolean startsWith(byte[] data, byte[] prefix) {
if (data.length < prefix.length) {
return false;
}
for (int i = 0; i < prefix.length; i++) {
if (data[i] != prefix[i]) {
return false;
}
}
return true;
}
}
}
@@ -0,0 +1,146 @@
/*
* 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.crypto.encrypt;
import javax.crypto.SecretKey;
import javax.crypto.spec.PBEKeySpec;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AesGcmBytesEncryptor}.
*/
class AesGcmBytesEncryptorTests {
private final String secret = "value";
private final String password = "password";
private final String hexSalt = "deadbeef";
@Test
void roundtripWhenUsingPasswordAndSaltThenEncryptsAndDecrypts() {
CryptoAssumptions.assumeGCMJCE();
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() {
CryptoAssumptions.assumeGCMJCE();
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
SecretKey secretKey = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(secretKey).build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void encryptWhenUsingMockIvThenProducesKnownCiphertext() {
CryptoAssumptions.assumeGCMJCE();
BytesKeyGenerator mockGenerator = mock(BytesKeyGenerator.class);
given(mockGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
given(mockGenerator.getKeyLength()).willReturn(16);
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt)
.ivGenerator(mockGenerator)
.build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(Hex.encode(encrypted))
.isEqualTo("4b0febebd439db7ca77153cb254520c3e4d61ae38207b4e42b820d311dc3d4e0e2f37ed5ee");
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void encryptProducesUniqueOutputAndIvIsPrepended() {
CryptoAssumptions.assumeGCMJCE();
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] first = encryptor.encrypt(this.secret.getBytes());
byte[] second = encryptor.encrypt(this.secret.getBytes());
assertThat(first).isNotEqualTo(second);
assertThat(first.length).isGreaterThan(32);
}
@Test
@SuppressWarnings("deprecation")
void withSecretWhenAesBytesEncryptorEncryptsThenDecrypts() {
CryptoAssumptions.assumeGCMJCE();
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
AesBytesEncryptor.CipherAlgorithm.GCM);
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(key).build();
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
@SuppressWarnings("deprecation")
void aesBytesEncryptorWhenEncryptsThenAesGcmBytesEncryptorDecrypts() {
CryptoAssumptions.assumeGCMJCE();
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(key)
.ivGenerator(KeyGenerators.secureRandom(12))
.build();
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(12),
AesBytesEncryptor.CipherAlgorithm.GCM);
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(deprecated.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
void roundtripWhenUsingCustomIvGeneratorLengthThenEncryptsAndDecrypts() {
CryptoAssumptions.assumeGCMJCE();
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt)
.ivGenerator(KeyGenerators.secureRandom(12))
.build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
}
@Test
@SuppressWarnings("deprecation")
void withPasswordDerivesADifferentKeyThanAesBytesEncryptor() {
CryptoAssumptions.assumeGCMJCE();
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt,
KeyGenerators.secureRandom(16), AesBytesEncryptor.CipherAlgorithm.GCM);
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
}
@Test
void decryptDetectsAuthenticationTagTampering() {
CryptoAssumptions.assumeGCMJCE();
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
encrypted[17] ^= 0xFF;
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
}
}
@@ -24,25 +24,23 @@ import javax.crypto.NoSuchPaddingException;
import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Assumptions;
import org.opentest4j.TestAbortedException; import org.opentest4j.TestAbortedException;
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
public final class CryptoAssumptions { public final class CryptoAssumptions {
private CryptoAssumptions() { private CryptoAssumptions() {
} }
public static void assumeGCMJCE() { public static void assumeGCMJCE() {
assumeAes256(CipherAlgorithm.GCM); assumeAes256("GCM");
} }
public static void assumeCBCJCE() { public static void assumeCBCJCE() {
assumeAes256(CipherAlgorithm.CBC); assumeAes256("CBC");
} }
private static void assumeAes256(CipherAlgorithm cipherAlgorithm) { private static void assumeAes256(String cipherAlgorithm) {
boolean aes256Available = false; boolean aes256Available = false;
try { try {
Cipher.getInstance(cipherAlgorithm.toString()); Cipher.getInstance(cipherAlgorithm);
aes256Available = Cipher.getMaxAllowedKeyLength("AES") >= 256; aes256Available = Cipher.getMaxAllowedKeyLength("AES") >= 256;
} }
catch (NoSuchAlgorithmException ex) { catch (NoSuchAlgorithmException ex) {
@@ -8,8 +8,8 @@ The code is distributed as part of the core module but has no dependencies on an
[[spring-security-crypto-encryption]] [[spring-security-crypto-encryption]]
== Encryptors == Encryptors
The javadoc:org.springframework.security.crypto.encrypt.Encryptors[] class provides factory methods for constructing symmetric encryptors. Spring Security provides javadoc:org.springframework.security.crypto.encrypt.AesGcmBytesEncryptor[] and javadoc:org.springframework.security.crypto.encrypt.AesCbcBytesEncryptor[] for constructing symmetric encryptors.
This class lets you create javadoc:org.springframework.security.crypto.encrypt.BytesEncryptor[] instances to encrypt data in raw `byte[]` form. These can be used to encrypt data in raw `byte[]` form.
You can also construct javadoc:org.springframework.security.crypto.encrypt.TextEncryptor[] instances to encrypt text strings. You can also construct javadoc:org.springframework.security.crypto.encrypt.TextEncryptor[] instances to encrypt text strings.
Encryptors are thread-safe. Encryptors are thread-safe.
@@ -20,7 +20,7 @@ Both `BytesEncryptor` and `TextEncryptor` are interfaces. `BytesEncryptor` has m
[[spring-security-crypto-encryption-bytes]] [[spring-security-crypto-encryption-bytes]]
=== BytesEncryptor === BytesEncryptor
You can use the `Encryptors.stronger` factory method to construct a `BytesEncryptor`: Use `AesGcmBytesEncryptor` to construct a BytesEncryptor with authenticated encryption:
.BytesEncryptor .BytesEncryptor
[tabs] [tabs]
@@ -29,24 +29,22 @@ Java::
+ +
[source,java,role="primary"] [source,java,role="primary"]
---- ----
Encryptors.stronger("password", "salt"); AesGcmBytesEncryptor.withPassword("password", "salt").build();
---- ----
Kotlin:: Kotlin::
+ +
[source,kotlin,role="secondary"] [source,kotlin,role="secondary"]
---- ----
Encryptors.stronger("password", "salt") AesGcmBytesEncryptor.withPassword("password", "salt").build()
---- ----
====== ======
The `stronger` encryption method creates an encryptor by using 256-bit AES encryption with `AesGcmBytesEncryptor` uses 256-bit AES encryption with Galois Counter Mode (GCM), providing https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated encryption] (AEAD).
Galois Counter Mode (GCM). It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
It derives the secret key by using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2). The password used to generate the SecretKey should be kept in a secure place and not be shared.
This method requires Java 6. The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
The password used to generate the `SecretKey` should be kept in a secure place and should not be shared. A 16-byte random initialization vector is also applied so each encrypted message is unique.
The salt is used to prevent dictionary attacks against the key in the event that your encrypted data is compromised.
A 16-byte random initialization vector is also applied so that each encrypted message is unique.
The provided salt should be in hex-encoded String form, be random, and be at least 8 bytes in length. The provided salt should be in hex-encoded String form, be random, and be at least 8 bytes in length.
You can generate such a salt by using a `KeyGenerator`: You can generate such a salt by using a `KeyGenerator`:
@@ -69,14 +67,33 @@ val salt = KeyGenerators.string().generateKey() // generates a random 8-byte sal
---- ----
====== ======
You can also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode. Users who require CBC mode may use `AesCbcBytesEncryptor`:
.AesCbcBytesEncryptor
[tabs]
======
Java::
+
[source,java,role="primary"]
----
AesCbcBytesEncryptor.withPassword("password", "salt").build();
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
AesCbcBytesEncryptor.withPassword("password", "salt").build()
----
======
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
guarantees about the authenticity of the data. guarantees about the authenticity of the data.
For a more secure alternative, use `Encryptors.stronger`. For a more secure alternative, users should prefer `AesGcmBytesEncryptor`.
[[spring-security-crypto-encryption-text]] [[spring-security-crypto-encryption-text]]
=== TextEncryptor === TextEncryptor
You can use the `Encryptors.text` factory method to construct a standard TextEncryptor: Use `AesCbcBytesEncryptor` to encrypt text data:
.TextEncryptor .TextEncryptor
[tabs] [tabs]
@@ -85,19 +102,22 @@ Java::
+ +
[source,java,role="primary"] [source,java,role="primary"]
---- ----
Encryptors.text("password", "salt"); AesCbcBytesEncryptor.withPassword("password", "salt").build();
---- ----
Kotlin:: Kotlin::
+ +
[source,kotlin,role="secondary"] [source,kotlin,role="secondary"]
---- ----
Encryptors.text("password", "salt") AesCbcBytesEncryptor.withPassword("password", "salt").build()
---- ----
====== ======
A `TextEncryptor` uses a standard `BytesEncryptor` to encrypt text data. `AesCbcBytesEncryptor` encrypts data as raw bytes.
Encrypted results are returned as hex-encoded strings for easy storage on the filesystem or in a database. You can hex-encode the result for easy storage on the filesystem or in the database.
NOTE: Queryable text encryption (encrypting such that the same plaintext always produces the same ciphertext) is no longer recommended, as it relies on a fixed initialization vector and does not provide adequate security.
Instead, look to your data store for a mechanism to query encrypted data.
[[spring-security-crypto-keygenerators]] [[spring-security-crypto-keygenerators]]
== Key Generators == Key Generators
@@ -54,138 +54,13 @@ Therefore, a custom javadoc:org.springframework.security.web.server.authenticati
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc. One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
In the following example, we are going to create a magic link and sent it to the user's email. In the following example, we are going to create a magic link and sent it to the user's email.
.One-Time Token Login Configuration include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component <1>
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final ServerOneTimeTokenGenerationSuccessHandler redirectHandler = new ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
// constructor omitted
@Override
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
return Mono.just(exchange.getRequest())
.map((request) ->
UriComponentsBuilder.fromUri(request.getURI())
.replacePath(request.getPath().contextPath().value())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue())
.toUriString() <2>
)
.flatMap((uri) -> this.mailSender.send(getUserEmail(oneTimeToken.getUsername()), <3>
"Use the following link to sign in into the application: " + magicLink)) <4>
.then(this.redirectHandler.handle(exchange, oneTimeToken)); <5>
}
private String getUserEmail() {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oneTimeTokenLogin { }
}
}
}
@Component (1)
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
private val redirectStrategy: ServerRedirectStrategy = DefaultServerRedirectStrategy()
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
val builder = UriComponentsBuilder.fromUri(exchange.request.uri)
.replacePath(null)
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()) (2)
val magicLink = builder.toUriString()
builder.replacePath(null)
.replaceQuery(null)
.path("/ott/sent")
val redirectLink = builder.toUriString()
return this.mailSender.send(
getUserEmail(oneTimeToken.getUsername()), (3)
"Use the following link to sign in into the application: $magicLink") (4)
.then(this.redirectStrategy.sendRedirect(exchange, URI.create(redirectLink))) (5)
}
private String getUserEmail() {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
----
======
<1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean <1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
<2> Create a login processing URL with the `token` as a query param <2> Create a login processing URL with the `token` as a query param
<3> Retrieve the user's email based on the username <3> Retrieve the user's email based on the username
<4> Use the `MailSender` API to send the email to the user with the magic link <4> Use the `MailSender` API to send the email to the user with the magic link
<5> Use the `ServerRedirectStrategy` to perform a redirect to your desired URL <5> Use the `ServerOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
The email content will look similar to: The email content will look similar to:
@@ -197,65 +72,10 @@ The default submit page will detect that the URL has the `token` query param and
== Changing the One-Time Token Generate URL == Changing the One-Time Token Generate URL
By default, the javadoc:org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter[] listens to `POST /ott/generate` requests. By default, the javadoc:org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter[] listens to `POST /ott/generate` requests.
That URL can be changed by using the `generateTokenUrl(String)` DSL method: That URL can be changed by using the `tokenGeneratingUrl(String)` DSL method:
.Changing the Generate URL include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.generateTokenUrl("/ott/my-generate-url")
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
formLogin { }
oneTimeTokenLogin {
generateTokenUrl = "/ott/my-generate-url"
}
}
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[changing-submit-page-url]] [[changing-submit-page-url]]
== Changing the Default Submit Page URL == Changing the Default Submit Page URL
@@ -263,151 +83,17 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender):
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] and listens to `GET /login/ott`. The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] and listens to `GET /login/ott`.
The URL can also be changed, like so: The URL can also be changed, like so:
.Configuring the Default Submit Page URL include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.submitPageUrl("/ott/submit")
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
formLogin { }
oneTimeTokenLogin {
submitPageUrl = "/ott/submit"
}
}
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[disabling-default-submit-page]] [[disabling-default-submit-page]]
== Disabling the Default Submit Page == Disabling the Default Submit Page
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint. If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
.Disabling the Default Submit Page include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
====== include-code::./MyController[tag=snippet,indent=0]
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.pathMatchers("/my-ott-submit").permitAll()
.anyExchange().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(pathMatchers("/my-ott-submit"), permitAll)
authorize(anyExchange, authenticated)
}
.formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
}
}
@Controller
class MyController {
@GetMapping("/my-ott-submit")
fun ottSubmitPage(): String {
return "my-ott-submit"
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[customize-generate-consume-token]] [[customize-generate-consume-token]]
== Customize How to Generate and Consume One-Time Tokens == Customize How to Generate and Consume One-Time Tokens
@@ -425,160 +111,21 @@ Some of the most common reasons to customize the `ReactiveOneTimeTokenService` a
There are two options to customize the `ReactiveOneTimeTokenService`. There are two options to customize the `ReactiveOneTimeTokenService`.
One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL: One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
.Passing the ReactiveOneTimeTokenService as a Bean include-code::./OneTimeTokenServiceBeanSecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public ReactiveOneTimeTokenService oneTimeTokenService() {
return new MyCustomReactiveOneTimeTokenService();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
//..
.formLogin { }
oneTimeTokenLogin { }
}
}
@Bean
open fun oneTimeTokenService():ReactiveOneTimeTokenService {
return MyCustomReactiveOneTimeTokenService();
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
The second option is to pass the `ReactiveOneTimeTokenService` instance to the DSL, which is useful if there are multiple ``SecurityWebFilterChain``s and a different ``ReactiveOneTimeTokenService``s is needed for each of them. The second option is to pass the `ReactiveOneTimeTokenService` instance to the DSL, which is useful if there are multiple ``SecurityWebFilterChain``s and a different ``ReactiveOneTimeTokenService``s is needed for each of them.
.Passing the ReactiveOneTimeTokenService using the DSL include-code::./OneTimeTokenServiceDSLSecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.oneTimeTokenService(new MyCustomReactiveOneTimeTokenService())
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
//..
.formLogin { }
oneTimeTokenLogin {
oneTimeTokenService = MyCustomReactiveOneTimeTokenService()
}
}
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[customize-generate-token-request]] [[customize-generate-token-request]]
== Customize GenerateOneTimeTokenRequest Instance == Customize GenerateOneTimeTokenRequest Instance
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default. There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a @Bean, like so: You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a `@Bean`, like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
resolver.setExpiresIn(Duration.ofSeconds(600));
return resolver;
}
----
Kotlin:: include-code::./SecurityConfig[tag=config,indent=0]
+
[source,kotlin,role="secondary"]
----
@Bean
fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
----
======
[[validating-account-status]] [[validating-account-status]]
== Validating Account Status == Validating Account Status
@@ -54,133 +54,12 @@ Therefore, a custom javadoc:org.springframework.security.web.authentication.ott.
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc. One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
In the following example, we are going to create a magic link and sent it to the user's email. In the following example, we are going to create a magic link and sent it to the user's email.
.One-Time Token Login Configuration include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component <1>
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
// constructor omitted
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(request.getRequestURL().toString())
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()); <2>
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); <3>
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); <4>
this.redirectHandler.handle(request, response, oneTimeToken); <5>
}
private String getUserEmail() {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http{
formLogin {}
oneTimeTokenLogin { }
}
return http.build()
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component (1)
class MagicLinkOneTimeTokenGenerationSuccessHandler(
private val mailSender: MailSender,
private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
) : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
val builder = UriComponentsBuilder.fromUriString(request.getRequestURL().toString())
.replacePath(request.contextPath)
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()) (2)
val magicLink = builder.toUriString()
val email = getUserEmail(oneTimeToken.getUsername()) (3)
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4)
this.redirectHandler.handle(request, response, oneTimeToken) (5)
}
private fun getUserEmail(): String {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
----
======
<1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean <1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
<2> Create a login processing URL with the `token` as a query param <2> Create a login processing URL with the `token` as a query param
<3> Retrieve the user's email based on the username <3> Retrieve the user's email based on the username
<4> Use the `JavaMailSender` API to send the email to the user with the magic link <4> Use the `MailSender` API to send the email to the user with the magic link
<5> Use the `RedirectOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL <5> Use the `RedirectOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
The email content will look similar to: The email content will look similar to:
@@ -193,70 +72,15 @@ The default submit page will detect that the URL has the `token` query param and
== Changing the One-Time Token Generate URL == Changing the One-Time Token Generate URL
By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to `POST /ott/generate` requests. By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to `POST /ott/generate` requests.
That URL can be changed by using the `generateTokenUrl(String)` DSL method: That URL can be changed by using the `tokenGeneratingUrl(String)` DSL method:
.Changing the Generate URL include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenGeneratingUrl("/ott/my-generate-url")
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
tokenGeneratingUrl = "/ott/my-generate-url"
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[NOTE] [NOTE]
==== ====
The URI passed to `generateTokenUrl` is matched literally. The URI passed to `tokenGeneratingUrl` is matched literally.
If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `generateTokenUrl("/api/ott/generate")`. If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `tokenGeneratingUrl("/api/ott/generate")`.
The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page. The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page.
==== ====
@@ -266,151 +90,17 @@ The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on t
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] and listens to `GET /login/ott`. The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] and listens to `GET /login/ott`.
The URL can also be changed, like so: The URL can also be changed, like so:
.Configuring the Default Submit Page URL include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.defaultSubmitPageUrl("/ott/submit")
);
return http.build();
}
}
@Component
public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
defaultSubmitPageUrl = "/ott/submit"
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[disabling-default-submit-page]] [[disabling-default-submit-page]]
== Disabling the Default Submit Page == Disabling the Default Submit Page
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint. If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
.Disabling the Default Submit Page include-code::./SecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
====== include-code::./MyController[tag=snippet,indent=0]
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my-ott-submit").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
@Component
public class OneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/my-ott-submit", authenticated)
authorize(anyRequest, authenticated)
}
formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
return http.build()
}
}
@Controller
class MyController {
@GetMapping("/my-ott-submit")
fun ottSubmitPage(): String {
return "my-ott-submit"
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[customize-generate-consume-token]] [[customize-generate-consume-token]]
== Customize How to Generate and Consume One-Time Tokens == Customize How to Generate and Consume One-Time Tokens
@@ -429,164 +119,21 @@ Some of the most common reasons to customize the `OneTimeTokenService` are, but
There are two options to customize the `OneTimeTokenService`. There are two options to customize the `OneTimeTokenService`.
One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL: One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
.Passing the OneTimeTokenService as a Bean include-code::./OneTimeTokenServiceBeanSecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new MyCustomOneTimeTokenService();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin { }
}
return http.build()
}
@Bean
open fun oneTimeTokenService(): OneTimeTokenService {
return MyCustomOneTimeTokenService()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
The second option is to pass the `OneTimeTokenService` instance to the DSL, which is useful if there are multiple `SecurityFilterChain` and a different `OneTimeTokenService` is needed for each of them. The second option is to pass the `OneTimeTokenService` instance to the DSL, which is useful if there are multiple `SecurityFilterChain` and a different `OneTimeTokenService` is needed for each of them.
.Passing the OneTimeTokenService using the DSL include-code::./OneTimeTokenServiceDSLSecurityConfig[tag=config,indent=0]
[tabs] include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.oneTimeTokenService(new MyCustomOneTimeTokenService())
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
oneTimeTokenService = MyCustomOneTimeTokenService()
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
----
======
[[customize-generate-token-request]] [[customize-generate-token-request]]
== Customize GenerateOneTimeTokenRequest Instance == Customize GenerateOneTimeTokenRequest Instance
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default. There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
You can customize elements of GenerateOneTimeTokenRequest by publishing an GenerateOneTimeTokenRequestResolver as a @Bean, like so: You can customize elements of GenerateOneTimeTokenRequest by publishing an GenerateOneTimeTokenRequestResolver as a `@Bean`, like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
return (request) -> {
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
};
}
----
Kotlin:: include-code::./SecurityConfig[tag=config,indent=0]
+
[source,kotlin,role="secondary"]
----
@Bean
fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
return DefaultGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
----
======
[[validating-account-status]] [[validating-account-status]]
== Validating Account Status == Validating Account Status
+5
View File
@@ -8,3 +8,8 @@
== Web == Web
* Since Spring Framework's `HttpMethod#valueOf` now normalizes casing, `StrictServerWebExchangeFirewall` no longer detects a non-canonical-case HTTP method (for example, `get` instead of `GET`) as a distinct value; such requests are processed as the canonical method instead of being rejected. Applications with a customized `ServerExchangeRejectedHandler` should be aware it is no longer invoked for this case. * Since Spring Framework's `HttpMethod#valueOf` now normalizes casing, `StrictServerWebExchangeFirewall` no longer detects a non-canonical-case HTTP method (for example, `get` instead of `GET`) as a distinct value; such requests are processed as the canonical method instead of being rejected. Applications with a customized `ServerExchangeRejectedHandler` should be aware it is no longer invoked for this case.
== OAuth 2.0
* https://github.com/spring-projects/spring-security/pull/18895[gh-18895] - Add `authenticationSuccessHandler` to the Reactive Resource Server DSL
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"dependencies": { "dependencies": {
"antora": "3.2.0-rc.2", "antora": "3.2.0-rc.3",
"@antora/atlas-extension": "1.0.0-alpha.5", "@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.3", "@antora/collector-extension": "1.0.3",
"@asciidoctor/tabs": "1.0.0-beta.6", "@asciidoctor/tabs": "1.0.0-beta.6",
+1
View File
@@ -49,6 +49,7 @@ dependencies {
testImplementation 'org.springframework:spring-websocket' testImplementation 'org.springframework:spring-websocket'
testImplementation 'org.springframework:spring-webmvc' testImplementation 'org.springframework:spring-webmvc'
testImplementation 'org.springframework:spring-context-support'
testImplementation 'jakarta.servlet:jakarta.servlet-api' testImplementation 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'io.mockk:mockk' testImplementation 'io.mockk:mockk'
testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-api"
@@ -0,0 +1,35 @@
/*
* 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.docs.reactive.authentication.changinggenerateurl;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
/**/ return Mono.empty();
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* 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.docs.reactive.authentication.changinggenerateurl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
// tag::config[]
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenGeneratingUrl("/ott/my-generate-url")
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.docs.reactive.authentication.changingsubmitpageurl;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
/**/ return Mono.empty();
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* 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.docs.reactive.authentication.changingsubmitpageurl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
// tag::config[]
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.defaultSubmitPageUrl("/ott/submit")
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
/**/ return Mono.empty();
}
}
// end::snippet[]
@@ -0,0 +1,38 @@
/*
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
import reactor.core.publisher.Mono;
class MyCustomReactiveOneTimeTokenService implements ReactiveOneTimeTokenService {
@Override
public Mono<OneTimeToken> generate(GenerateOneTimeTokenRequest request) {
return null;
}
@Override
public Mono<OneTimeToken> consume(OneTimeTokenAuthenticationToken authenticationToken) {
return null;
}
}
@@ -0,0 +1,48 @@
/*
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.kt.docs.reactive.authentication.customizegenerateconsumetoken.MyCustomReactiveOneTimeTokenService;
import org.springframework.security.web.server.SecurityWebFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class OneTimeTokenServiceBeanSecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public ReactiveOneTimeTokenService oneTimeTokenService() {
return new MyCustomReactiveOneTimeTokenService();
}
}
// end::config[]
@@ -0,0 +1,43 @@
/*
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
// tag::config[]
@Configuration
@EnableWebFluxSecurity
public class OneTimeTokenServiceDSLSecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenService(new MyCustomReactiveOneTimeTokenService())
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.docs.reactive.authentication.customizegeneratetokenrequest;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
public class SecurityConfig {
// tag::config[]
@Bean
ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
resolver.setExpiresIn(Duration.ofMinutes(10));
return resolver;
}
// end::config[]
}
@@ -0,0 +1,35 @@
/*
* 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.docs.reactive.authentication.disablingdefaultsubmitpage;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
/**/ return Mono.empty();
}
}
// end::snippet[]
@@ -0,0 +1,32 @@
/*
* 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.docs.reactive.authentication.disablingdefaultsubmitpage;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
// tag::snippet[]
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
// end::snippet[]
@@ -0,0 +1,46 @@
/*
* 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.docs.reactive.authentication.disablingdefaultsubmitpage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
// tag::config[]
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.pathMatchers("/my-ott-submit").permitAll()
.anyExchange().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,84 @@
/*
* 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.docs.reactive.authentication.sendingtokentouser;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
// tag::snippet[]
@Component // <1>
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final ServerOneTimeTokenGenerationSuccessHandler redirectHandler = new ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
public MagicLinkOneTimeTokenGenerationSuccessHandler(MailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
return Mono.just(exchange.getRequest())
.map((request) ->
UriComponentsBuilder.fromUri(request.getURI())
.replacePath(request.getPath().contextPath().value())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue())
.toUriString() // <2>
)
.flatMap((uri) -> {
String email = getUserEmail(oneTimeToken.getUsername()); // <3>
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("Your Spring Security One Time Token");
message.setText("Use the following link to sign in into the application: " + uri);
this.mailSender.send(message); // <4>
return Mono.empty();
})
.then(this.redirectHandler.handle(exchange, oneTimeToken)); // <5>
}
private String getUserEmail(String username) {
/**/ return username;
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
// end::snippet[]
@@ -0,0 +1,41 @@
/*
* 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.docs.reactive.authentication.sendingtokentouser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
// tag::config[]
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.docs.servlet.authentication.changinggenerateurl;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* 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.docs.servlet.authentication.changinggenerateurl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenGeneratingUrl("/ott/my-generate-url")
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.docs.servlet.authentication.changingsubmitpageurl;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* 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.docs.servlet.authentication.changingsubmitpageurl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.defaultSubmitPageUrl("/ott/submit")
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,37 @@
/*
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
import org.springframework.security.authentication.ott.OneTimeTokenService;
class MyCustomOneTimeTokenService implements OneTimeTokenService {
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
return null;
}
@Override
public @Nullable OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) {
return null;
}
}
@@ -0,0 +1,47 @@
/*
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ott.OneTimeTokenService;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class OneTimeTokenServiceBeanSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new MyCustomOneTimeTokenService();
}
}
// end::config[]
@@ -0,0 +1,43 @@
/*
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class OneTimeTokenServiceDSLSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenService(new MyCustomOneTimeTokenService())
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,39 @@
/*
* 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.docs.servlet.authentication.customizegeneratetokenrequest;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
import java.time.Duration;
public class SecurityConfig {
// tag::config[]
@Bean
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
return (request) -> {
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofMinutes(10));
};
}
// end::config[]
}
@@ -0,0 +1,35 @@
/*
* 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.docs.servlet.authentication.disablingdefaultsubmitpage;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
// tag::snippet[]
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,32 @@
/*
* 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.docs.servlet.authentication.disablingdefaultsubmitpage;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
// tag::snippet[]
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
// end::snippet[]
@@ -0,0 +1,46 @@
/*
* 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.docs.servlet.authentication.disablingdefaultsubmitpage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my-ott-submit").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
// end::config[]
@@ -0,0 +1,80 @@
/*
* 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.docs.servlet.authentication.sendingtokentouser;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
// tag::snippet[]
@Component // <1>
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
public MagicLinkOneTimeTokenGenerationSuccessHandler(MailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()); // <2>
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); // <3>
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("Your Spring Security One Time Token");
message.setText("Use the following link to sign in into the application: " + magicLink);
this.mailSender.send(message); // <4>
this.redirectHandler.handle(request, response, oneTimeToken); // <5>
}
private String getUserEmail(String username) {
/**/ return username;
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
// end::snippet[]
@@ -0,0 +1,41 @@
/*
* 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.docs.servlet.authentication.sendingtokentouser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::config[]
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
// end::config[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.reactive.authentication.changinggenerateurl
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
/**/ return Mono.empty()
}
}
// end::snippet[]
@@ -0,0 +1,41 @@
/*
* 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.kt.docs.reactive.authentication.changinggenerateurl
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain
// tag::config[]
@Configuration
@EnableWebFluxSecurity
open class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
formLogin { }
oneTimeTokenLogin {
tokenGeneratingUrl = "/ott/my-generate-url"
}
}
}
}
// end::config[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.reactive.authentication.changingsubmitpageurl
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
/**/ return Mono.empty()
}
}
// end::snippet[]
@@ -0,0 +1,41 @@
/*
* 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.kt.docs.reactive.authentication.changingsubmitpageurl
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain
// tag::config[]
@Configuration
@EnableWebFluxSecurity
open class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
formLogin { }
oneTimeTokenLogin {
defaultSubmitPageUrl = "/ott/submit"
}
}
}
}
// end::config[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
/**/ return Mono.empty()
}
}
// end::snippet[]
@@ -0,0 +1,36 @@
/*
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
import reactor.core.publisher.Mono
class MyCustomReactiveOneTimeTokenService: ReactiveOneTimeTokenService {
override fun generate(request: GenerateOneTimeTokenRequest): Mono<OneTimeToken> {
TODO("Not yet implemented")
}
override fun consume(authenticationToken: OneTimeTokenAuthenticationToken): Mono<OneTimeToken> {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,46 @@
/*
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain
// tag::config[]
@Configuration
@EnableWebFluxSecurity
open class OneTimeTokenServiceBeanSecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
//..
formLogin { }
oneTimeTokenLogin { }
}
}
@Bean
open fun oneTimeTokenService(): ReactiveOneTimeTokenService {
return MyCustomReactiveOneTimeTokenService()
}
}
// end::config[]
@@ -0,0 +1,41 @@
/*
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain
// tag::config[]
@Configuration
@EnableWebFluxSecurity
open class OneTimeTokenServiceDSLSecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
//..
formLogin { }
oneTimeTokenLogin {
tokenService = MyCustomReactiveOneTimeTokenService()
}
}
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.kt.docs.reactive.authentication.customizegeneratetokenrequest
import org.springframework.context.annotation.Bean
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
import java.time.Duration
open class SecurityConfig {
// tag::config[]
@Bean
fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
// end::config[]
}
@@ -0,0 +1,35 @@
/*
* 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.kt.docs.reactive.authentication.disablingdefaultsubmitpage
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
/**/ return Mono.empty()
}
}
// end::snippet[]
@@ -0,0 +1,32 @@
/*
* 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.kt.docs.reactive.authentication.disablingdefaultsubmitpage
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
// tag::snippet[]
@Controller
class MyController {
@GetMapping("/my-ott-submit")
fun ottSubmitPage(): String {
return "my-ott-submit"
}
}
// end::snippet[]
@@ -0,0 +1,45 @@
/*
* 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.kt.docs.reactive.authentication.disablingdefaultsubmitpage
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers
// tag::config[]
@Configuration
@EnableWebFluxSecurity
open class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(pathMatchers("/my-ott-submit"), permitAll)
authorize(anyExchange, authenticated)
}
formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
}
}
// end::config[]
@@ -0,0 +1,78 @@
/*
* 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.kt.docs.reactive.authentication.sendingtokentouser
import org.springframework.mail.MailSender
import org.springframework.mail.SimpleMailMessage
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.util.UriComponentsBuilder
import reactor.core.publisher.Mono
import java.util.function.Function
// tag::snippet[]
@Component // <1>
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender) : ServerOneTimeTokenGenerationSuccessHandler {
private val redirectHandler: ServerOneTimeTokenGenerationSuccessHandler = ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
return Mono.just(exchange.request)
.map(Function { request ->
UriComponentsBuilder.fromUri(request.uri)
.replacePath(request.path.contextPath().value())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue())
.toUriString() // <2>
})
.flatMap(Function { uri ->
val email = getUserEmail(oneTimeToken.getUsername()) // <3>
val message = SimpleMailMessage()
message.setTo(email)
message.subject = "Your Spring Security One Time Token"
message.text = "Use the following link to sign in into the application: $uri"
this.mailSender.send(message) // <4>
Mono.empty()
})
.then(this.redirectHandler.handle(exchange, oneTimeToken)) // <5>
}
private fun getUserEmail(username: String): String {
/**/ return username
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
// end::snippet[]
@@ -0,0 +1,40 @@
/*
* 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.kt.docs.reactive.authentication.sendingtokentouser
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain
// tag::config[]
@Configuration
@EnableWebFluxSecurity
open class SecurityConfig {
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oneTimeTokenLogin { }
}
}
}
// end::config[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.servlet.authentication.changinggenerateurl
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* 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.kt.docs.servlet.authentication.changinggenerateurl
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
// tag::config[]
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
tokenGeneratingUrl = "/ott/my-generate-url"
}
}
return http.build()
}
}
// end::config[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.servlet.authentication.changingsubmitpageurl
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* 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.kt.docs.servlet.authentication.changingsubmitpageurl
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
// tag::config[]
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
defaultSubmitPageUrl = "/ott/submit"
}
}
return http.build()
}
}
// end::config[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
// end::snippet[]
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken
import org.springframework.security.authentication.ott.OneTimeTokenService
class MyCustomOneTimeTokenService: OneTimeTokenService {
override fun generate(request: GenerateOneTimeTokenRequest): OneTimeToken {
TODO("Not yet implemented")
}
override fun consume(authenticationToken: OneTimeTokenAuthenticationToken): OneTimeToken? {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,48 @@
/*
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.ott.OneTimeTokenService
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
// tag::config[]
@Configuration
@EnableWebSecurity
open class OneTimeTokenServiceBeanSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin { }
}
return http.build()
}
@Bean
open fun oneTimeTokenService(): OneTimeTokenService {
return MyCustomOneTimeTokenService()
}
}
// end::config[]
@@ -0,0 +1,44 @@
/*
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
// tag::config[]
@Configuration
@EnableWebSecurity
open class OneTimeTokenServiceDSLSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
tokenService = MyCustomOneTimeTokenService()
}
}
return http.build()
}
}
// end::config[]
@@ -0,0 +1,35 @@
/*
* 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.kt.docs.servlet.authentication.customizegeneratetokenrequest
import org.springframework.context.annotation.Bean
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver
import java.time.Duration
open class SecurityConfig {
// tag::config[]
@Bean
open fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
return DefaultGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
// end::config[]
}
@@ -0,0 +1,34 @@
/*
* 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.kt.docs.servlet.authentication.disablingdefaultsubmitpage
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.ott.OneTimeToken
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
// tag::snippet[]
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
// end::snippet[]

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