1
0
mirror of synced 2026-09-05 08:49:54 +00:00

Compare commits

..

24 Commits

Author SHA1 Message Date
Josh Cummings a825937b81 Release 7.1.1
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings 74c91f8008 Update to Spring Data 2026.0.1
Closes gh-19490

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings b822214582 Update to Spring LDAP 4.1.1
Closes gh-19491

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings 39da00ff9c Update to Micrometer 1.17.1
Closes gh-19489

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings 4be80b754e Update to Reactor 2025.0.7
Closes gh-19488

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings 564a6775f6 Update to Spring Framework 7.0.9
Closes gh-19487

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings 293019846e Configure Build for Commercial Repositories
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings 25f9a9199b Configure with Commercial Workflows
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:51 -06:00
Josh Cummings bf8569f881 Remove OSS CI configuration 2026-08-13 16:33:51 -06:00
Josh Cummings dcee218cac 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-13 16:33:33 -06:00
Josh Cummings 952fc5a843 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-13 16:33:33 -06:00
Josh Cummings 31611d0873 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-13 16:33:33 -06:00
Josh Cummings a447020c92 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-13 16:33:33 -06:00
Joe Grandja 81d17a55a7 Bind UnboundIdContainer to loopback address 2026-08-13 16:33:33 -06:00
Joe Grandja 1bbbed7a32 Apply html encoding in DefaultConsentPage 2026-08-13 16:33:33 -06:00
Joe Grandja b47b96e6b4 Provide ability to configure DPoP proof replay 2026-08-13 16:33:33 -06:00
Josh Cummings deddcce169 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-13 16:33:33 -06:00
Josh Cummings f5bfdc4b79 Use Utf8#isEqual
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:33 -06:00
Josh Cummings 09ae8bee20 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-13 16:33:32 -06:00
Josh Cummings e809909d34 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-13 16:33:32 -06:00
Josh Cummings 2f06cd7e4d 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-13 16:33:32 -06:00
Josh Cummings bcbd2f7a73 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-13 16:33:32 -06:00
Josh Cummings 022bb82aab Add Tests for User and Device Code
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-13 16:33:32 -06:00
Josh Cummings 9fdcc6104d 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-13 16:33:32 -06:00
112 changed files with 1514 additions and 3075 deletions
@@ -0,0 +1,7 @@
name: Build Release
runs:
using: composite
steps:
- name: Build Release
shell: bash
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository publishAllPublicationsToDeploymentRepository
@@ -0,0 +1,7 @@
name: Test Release
runs:
using: composite
steps:
- name: Test Release
shell: bash
run: ./gradlew build
+17
View File
@@ -0,0 +1,17 @@
workflow:
generator:
project:
java:
versions:
primary: 25
workflows:
release-train:
build:
env:
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
test:
env:
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
-36
View File
@@ -1,36 +0,0 @@
name: CI
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
permissions:
contents: read
jobs:
snapshot-test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
strategy:
matrix:
include:
- java-version: 25
toolchain: 25
with:
java-version: ${{ matrix.java-version }}
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot,https://oss.sonatype.org/content/repositories/snapshots -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=7.0.+ -PreactorVersion=2025.+ -PspringDataVersion=2025.+ -PmicrometerVersion=1.+ --stacktrace
secrets: inherit
send-notification:
name: Send Notification
needs: [ snapshot-test ]
if: ${{ !success() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -1,23 +0,0 @@
name: Clean build artifacts
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Delete artifacts in cron job
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
echo "Running clean build artifacts logic"
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
echo Output is $output
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
-17
View File
@@ -1,17 +0,0 @@
name: "CodeQL Advanced"
on:
push:
pull_request:
workflow_dispatch:
schedule:
# https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule
- cron: '0 5 * * *'
permissions: read-all
jobs:
codeql-analysis-call:
permissions:
actions: read
contents: read
security-events: write
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
@@ -8,49 +8,46 @@ on:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
jdk: [ 25 ]
uses: spring-projects/spring-security-commercial/.github/workflows/build.yml@workflows/v1
with:
runs-on: ${{ matrix.os }}
java-version: ${{ matrix.jdk }}
distribution: temurin
java-version: '25'
secrets: inherit
test:
name: Test Against Snapshots
uses: spring-projects/spring-security-commercial/.github/workflows/test.yml@workflows/v1
with:
java-version: '25'
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=25 -PspringFrameworkVersion=7.0.+ -PreactorVersion=2025.0.+ -PspringDataVersion=2026.0.+ -PmicrometerVersion=1.17.+ --stacktrace
secrets: inherit
compute-version:
name: Compute Version
runs-on: ubuntu-latest
outputs:
snapshot: ${{ steps.project-version.outputs.snapshot }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0
- id: project-version
name: Extract Project Version
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
deploy-artifacts:
name: Deploy Artifacts
needs: [ build ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
default-publish-milestones-central: true
java-version: 25
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
java-version: 25
needs: [ build, test, compute-version ]
if: needs.compute-version.outputs.snapshot == 'true'
uses: spring-projects/spring-security-commercial/.github/workflows/deploy-artifacts.yml@workflows/v1
secrets: inherit
send-notification:
name: Send Notification
needs: [ deploy-artifacts, deploy-schema ]
needs: [ deploy-artifacts ]
if: ${{ !success() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-76
View File
@@ -1,76 +0,0 @@
name: Defer Issues
on:
workflow_dispatch:
permissions:
contents: read
jobs:
defer-issues:
name: Defer Issues
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
permissions:
issues: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Compute Version
id: compute-version
uses: spring-io/spring-release-actions/compute-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
- name: Get Today's Release Version
id: todays-release
uses: spring-io/spring-release-actions/get-todays-release-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
with:
snapshot-version: ${{ steps.compute-version.outputs.version }}
milestone-repository: ${{ github.repository }}
milestone-token: ${{ secrets.GITHUB_TOKEN }}
- name: Compute Next Version
id: next-version
uses: spring-io/spring-release-actions/compute-next-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
with:
version: ${{ steps.todays-release.outputs.release-version }}
- name: Schedule Next Milestone
uses: spring-io/spring-release-actions/schedule-milestone@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
with:
version: ${{ steps.next-version.outputs.version }}
version-date: ${{ steps.next-version.outputs.version-date }}
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Move Open Issues to Next Milestone
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT_MILESTONE: ${{ steps.todays-release.outputs.release-version }}
NEXT_MILESTONE: ${{ steps.next-version.outputs.version }}
run: |
current_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
--jq ".[] | select(.title == \"$CURRENT_MILESTONE\") | .number")
if [ -z "$current_milestone_number" ]; then
echo "No milestone found for $CURRENT_MILESTONE"
exit 0
fi
next_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
--jq ".[] | select(.title == \"$NEXT_MILESTONE\") | .number")
if [ -z "$next_milestone_number" ]; then
echo "No milestone found for $NEXT_MILESTONE"
exit 1
fi
echo "Moving open issues from milestone '$CURRENT_MILESTONE' (#$current_milestone_number) to '$NEXT_MILESTONE' (#$next_milestone_number)"
page=1
while true; do
issues=$(gh api "repos/${{ github.repository }}/issues?milestone=$current_milestone_number&state=open&per_page=100&page=$page" \
--jq '.[].number')
if [ -z "$issues" ]; then
break
fi
for issue in $issues; do
echo "Moving issue/PR #$issue to milestone $NEXT_MILESTONE"
gh api repos/${{ github.repository }}/issues/$issue \
--method PATCH \
--field milestone=$next_milestone_number \
--silent
done
page=$((page + 1))
done
echo "Done."
@@ -0,0 +1,10 @@
name: Dependabot PR Build
on: pull_request_target
jobs:
build:
name: Build
uses: spring-projects/spring-security-commercial/.github/workflows/build-pull-request.yml@workflows/v1
if: ${{ github.actor == 'dependabot[bot]' }}
secrets: inherit
-33
View File
@@ -1,33 +0,0 @@
name: Deploy Docs
on:
push:
branches-ignore:
- "gh-pages"
- "dependabot/**"
tags: '**'
repository_dispatch:
types: request-build-reference # legacy
#schedule:
#- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch:
permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: docs-build
fetch-depth: 1
- name: Dispatch (partial build)
if: github.ref_type == 'branch'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
- name: Dispatch (full build)
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
@@ -1,34 +0,0 @@
name: Execute Gradle Wrapper Upgrade
on:
schedule:
- cron: '0 2 * * *' # 2am UTC
workflow_dispatch:
permissions:
pull-requests: write
jobs:
upgrade_wrapper:
name: Execution
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- name: Set up Git configuration
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/"
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up JDK 25
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
java-version: '25'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
- name: Upgrade Wrappers
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
env:
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+63
View File
@@ -0,0 +1,63 @@
name: Merge Dependabot PR
on: pull_request_target
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions: write-all
jobs:
merge-dependabot-pr:
name: Merge Dependabot PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security-commercial' }}
steps:
- uses: actions/checkout@v5
with:
show-progress: false
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Set Milestone to Dependabot Pull Request
id: set-milestone
run: |
if test -f pom.xml
then
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
else
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
fi
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
if [ -z $MILESTONE ]
then
gh run cancel ${{ github.run_id }}
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
else
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
echo mergeEnabled=true >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot pull request
if: steps.set-milestone.outputs.mergeEnabled
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
send-notification:
name: Send Notification
needs: [ merge-dependabot-pr ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+3 -41
View File
@@ -8,44 +8,6 @@ permissions:
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: '25'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue --scan
generate-docs:
name: Generate Docs
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: '25'
distribution: 'temurin'
- name: Run Antora
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docs
path: docs/build/site
overwrite: true
send-notification:
name: Send Notification
needs: [ build, generate-docs ]
if: ${{ failure() && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
uses: spring-projects/spring-security-commercial/.github/workflows/build-pull-request.yml@workflows/v1
if: ${{ github.actor != 'dependabot[bot]' }}
secrets: inherit
@@ -1,30 +0,0 @@
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@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
with:
version: ${{ inputs.version }}
secrets: inherit
@@ -1,24 +0,0 @@
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
+92
View File
@@ -0,0 +1,92 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Build"
run-name: "${{ inputs.callback-ref }} Build"
"on":
workflow_dispatch:
inputs:
callback:
description: "Repository to which a callback should be made upon completion"
required: true
type: "string"
callback-ref:
description: "Ref in the callback repository to which a callback should be made upon completion"
required: true
type: "string"
release-train-maven-repository-url:
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
required: true
type: "string"
permissions:
contents: "read"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
jobs:
build-release:
name: "Build Release"
runs-on: "ubuntu22-2-8"
steps:
- name: "Prevent Re-runs"
id: "prevent-re-runs"
run: |-
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
echo "Re-runs are prohibited. Use the 'Release Train  Retry' workflow to retry build failures"
exit 1
fi
- name: "Set up Java"
id: "set-up-java"
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
with:
distribution: "liberica"
java-version: "25"
- name: "Check Out Code"
id: "check-out-code"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
- name: "Build Release"
id: "build-release"
uses: "./.github/actions/release-train-build"
env:
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
- name: "Upload Deployment Repository"
id: "upload-deployment-repository"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
name: "deployment-repository"
path: "deployment-repository/**"
- name: "Upload Deployment Spec"
id: "upload-deployment-spec"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
archive: "false"
if-no-files-found: "ignore"
name: "deployment-spec"
path: ".github/actions/release-train-build/deployment-spec.yml"
- name: "Save Build System Caches"
id: "save-build-system-caches"
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
with:
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
path: |-
~/.gradle/caches
~/.gradle/wrapper
- name: "Send Callback"
id: "send-callback"
if: "${{ !cancelled() }}"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run callback \
--repo ${{ inputs.callback }} \
--ref ${{ inputs.callback-ref }} \
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
--field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \
--field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }} \
--field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+55
View File
@@ -0,0 +1,55 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Join"
run-name: "${{ inputs.release-train }} Join"
"on":
workflow_dispatch:
inputs:
deployment-destination:
description: "Destination to which the release should be deployed"
options:
- "Maven Central"
- "Spring Enterprise"
required: true
type: "choice"
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
join-release-train:
name: "Join Release Train"
runs-on: "ubuntu-latest"
steps:
- name: "Join Release Train"
id: "join-release-train"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
run_url=$(
gh workflow run join \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field commit-hash=${{ github.sha }} \
--field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
)
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
+46
View File
@@ -0,0 +1,46 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Leave"
run-name: "${{ inputs.release-train }} Leave"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
leave:
name: "Leave"
runs-on: "ubuntu-latest"
steps:
- name: "Leave"
id: "leave"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
run_url=$(
gh workflow run leave \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
)
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
+47
View File
@@ -0,0 +1,47 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Ready"
run-name: "${{ inputs.release-train }} Ready"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
ready:
name: "Ready"
runs-on: "ubuntu-latest"
steps:
- name: "Ready"
id: "ready"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
run_url=$(
gh workflow run ready \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field commit-hash=${{ github.sha }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
)
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
+34
View File
@@ -0,0 +1,34 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Retry"
run-name: "${{ inputs.release-train }} Retry"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
trigger-retry:
name: "Trigger Retry"
runs-on: "ubuntu-latest"
steps:
- name: "Trigger Retry"
id: "trigger-retry"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run retry \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
+83
View File
@@ -0,0 +1,83 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Test"
run-name: "${{ inputs.callback-ref }} Test"
"on":
workflow_dispatch:
inputs:
callback:
description: "Repository to which a callback should be made upon completion"
required: true
type: "string"
callback-ref:
description: "Ref in the callback repository to which a callback should be made upon completion"
required: true
type: "string"
release-train-maven-repository-url:
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
required: true
type: "string"
permissions:
contents: "read"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
jobs:
test-release:
name: "Test Release"
runs-on: "ubuntu22-2-8"
steps:
- name: "Prevent Re-runs"
id: "prevent-re-runs"
run: |-
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
echo "Re-runs are prohibited. Use the 'Release Train  Retry' workflow to retry test failures"
exit 1
fi
- name: "Set up Java"
id: "set-up-java"
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
with:
distribution: "liberica"
java-version: "25"
- name: "Check Out Code"
id: "check-out-code"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
- name: "Restore Build System Caches"
id: "restore-build-system-caches"
uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
with:
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
path: |-
~/.gradle/caches
~/.gradle/wrapper
- name: "Test Release"
id: "test-release"
uses: "./.github/actions/release-train-test"
env:
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
- name: "Send Callback"
id: "send-callback"
if: "${{ !cancelled() }}"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run callback \
--repo ${{ inputs.callback }} \
--ref ${{ inputs.callback-ref }} \
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }} \
--field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "Upload Build System Reports"
id: "upload-build-system-reports"
if: "${{ failure() }}"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
name: "build-system-reports"
path: "**/build/reports"
@@ -1,37 +0,0 @@
name: Update Antora UI Spring
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch:
permissions:
pull-requests: write
issues: write
contents: write
jobs:
update-antora-ui-spring:
name: Update on Supported Branches
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
strategy:
matrix:
branch: [ '6.5.x', '7.0.x', 'main' ]
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
name: Update
with:
docs-branch: ${{ matrix.branch }}
token: ${{ secrets.GITHUB_TOKEN }}
antora-file-path: 'docs/antora-playbook.yml'
update-antora-ui-spring-docs-build:
name: Update on docs-build
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
name: Update
with:
docs-branch: 'docs-build'
token: ${{ secrets.GITHUB_TOKEN }}
+5 -8
View File
@@ -27,6 +27,7 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
apply plugin: 'org.springframework.security.check-expected-branch-version'
apply plugin: 'io.spring.security.release'
apply from: 'commercial-settings.gradle'
group = 'org.springframework.security'
description = 'Spring Security'
@@ -41,10 +42,12 @@ repositories {
}
springRelease {
repositoryOwner = "spring-projects"
repositoryName = "spring-security-commercial"
weekOfMonth = 3
dayOfWeek = 1
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
apiDocUrl = "https://docs.spring.io/spring-security/reference/{version}/api/java/index.html"
referenceDocUrl = "https://docs.spring.vmware.com/spring-security/reference/{version}/index.html"
apiDocUrl = "https://docs.spring.vmware.com/spring-security/reference/{version}/api/java/index.html"
replaceSnapshotVersionInReferenceDocUrl = true
}
@@ -72,12 +75,6 @@ allprojects {
}
}
develocity {
buildScan {
termsOfUseUrl = 'https://gradle.com/help/legal-terms-of-use'
termsOfUseAgree = 'yes'
}
}
nohttp {
source.exclude "buildSrc/build/**", "**/build/**", "**/target/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
+10 -1
View File
@@ -13,7 +13,16 @@ java {
repositories {
gradlePluginPortal()
mavenCentral()
maven { url = 'https://repo.spring.io/snapshot' }
if (project.hasProperty("artifactoryUsername") && project.hasProperty("artifactoryPassword")) {
maven {
name "spring-commercial-release"
url "https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"
credentials {
username project.artifactoryUsername
password project.artifactoryPassword
}
}
}
if (System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL") != null) {
maven {
name = "Release Train"
+27
View File
@@ -0,0 +1,27 @@
subprojects {
repositories {
mavenCentral()
def repoUsername = project.findProperty("artifactoryUsername") ?: System.getenv("COMMERCIAL_REPO_USERNAME")
def repoPassword = project.findProperty("artifactoryPassword") ?: System.getenv("COMMERCIAL_REPO_PASSWORD")
if (repoUsername && repoPassword) {
maven {
name "spring-commercial-release"
url "https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"
credentials {
username repoUsername
password repoPassword
}
}
if ("$version".endsWith("-SNAPSHOT")) {
maven {
name "spring-commercial-snapshot"
url "https://usw1.packages.broadcom.com/spring-enterprise-maven-dev-local"
credentials {
username repoUsername
password repoPassword
}
}
}
}
}
}
@@ -123,7 +123,7 @@ public final class PostFilterAuthorizationMethodInterceptor implements Authoriza
/**
* Filter a {@code returnedObject} using the {@link PostFilter} annotation that the
* {@link MethodInvocation} specifies.
* @param mi the {@link MethodInvocation} to check
* @param mi the {@link MethodInvocation} to check check
* @return filtered {@code returnedObject}
*/
@Override
@@ -131,7 +131,7 @@ public final class DelegatingSecurityContextCallable<V> implements Callable<V> {
/**
* Creates a {@link DelegatingSecurityContextCallable} and with the given
* {@link Callable} and {@link SecurityContext}, but if the securityContext is null
* will default to the current {@link SecurityContext} on the
* will defaults to the current {@link SecurityContext} on the
* {@link SecurityContextHolder}
* @param delegate the delegate {@link DelegatingSecurityContextCallable} to run with
* the specified {@link SecurityContext}. Cannot be null.
@@ -47,7 +47,7 @@ public final class DelegatingSecurityContextRunnable implements Runnable {
.getContextHolderStrategy();
/**
* The {@link SecurityContext} that the delegate {@link Runnable} will be run as.
* The {@link SecurityContext} that the delegate {@link Runnable} will be ran as.
*/
private SecurityContext delegateSecurityContext;
@@ -32,7 +32,6 @@ import org.springframework.util.Assert;
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath.
*
* @author Steve Riesenberg
* @author Tadaya Tsuyukubo
* @since 6.5
* @see io.micrometer.context.ContextRegistry
*/
@@ -54,9 +53,7 @@ public final class SecurityContextHolderThreadLocalAccessor implements ThreadLoc
@Override
public void setValue(SecurityContext securityContext) {
Assert.notNull(securityContext, "securityContext cannot be null");
SecurityContext newContext = SecurityContextHolder.createEmptyContext();
newContext.setAuthentication(securityContext.getAuthentication());
SecurityContextHolder.setContext(newContext);
SecurityContextHolder.setContext(securityContext);
}
@Override
@@ -35,7 +35,7 @@ import org.jspecify.annotations.Nullable;
* building block for more sophisticated token-based solutions. For example,
* authentication systems that depend on stateless session keys. These could, for
* 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.
* </p>
*
@@ -323,7 +323,7 @@ public class User implements UserDetails, CredentialsContainer {
/**
* Builds the user to be added. At minimum the username, password, and authorities
* should be provided. The remaining attributes have reasonable defaults.
* should provided. The remaining attributes have reasonable defaults.
*/
public static final class UserBuilder {
@@ -38,7 +38,7 @@ public interface UserDetailsService {
* may possibly be case sensitive, or case insensitive depending on how the
* 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
* was actually requested.
* was actually requested..
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @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
* a JDBC-based implementation.
* a a JDBC-based implementation.
*/
@NullMarked
package org.springframework.security.provisioning;
@@ -72,7 +72,7 @@ public class DelegatingSecurityContextAsyncTaskExecutor extends DelegatingSecuri
}
@Override
public final <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
public final <T> Future<T> submit(Callable<T> task) {
return getDelegate().submit(wrap(task));
}
@@ -16,18 +16,11 @@
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.BeforeEach;
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.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -36,7 +29,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link SecurityContextHolderThreadLocalAccessor}.
*
* @author Steve Riesenberg
* @author Tadaya Tsuyukubo
*/
public class SecurityContextHolderThreadLocalAccessorTests {
@@ -73,11 +65,9 @@ public class SecurityContextHolderThreadLocalAccessorTests {
@Test
public void setValueWhenSecurityContextThenSetsSecurityContextHolder() {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
Authentication authentication = new TestingAuthenticationToken("user", "password");
securityContext.setAuthentication(authentication);
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password"));
this.threadLocalAccessor.setValue(securityContext);
assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(authentication);
assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext);
}
@Test
@@ -100,41 +90,4 @@ public class SecurityContextHolderThreadLocalAccessorTests {
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);
}
}
@@ -53,13 +53,138 @@ 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.
In the following example, we are going to create a magic link and sent it to the user's email.
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.One-Time Token Login Configuration
[tabs]
======
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
<2> Create a login processing URL with the `token` as a query param
<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
<5> Use the `ServerOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
<5> Use the `ServerRedirectStrategy` to perform a redirect to your desired URL
The email content will look similar to:
@@ -71,10 +196,65 @@ The default submit page will detect that the URL has the `token` query param and
== 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.
That URL can be changed by using the `tokenGeneratingUrl(String)` DSL method:
That URL can be changed by using the `generateTokenUrl(String)` DSL method:
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Changing the Generate URL
[tabs]
======
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 the Default Submit Page URL
@@ -82,17 +262,151 @@ include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent
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:
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Configuring the Default Submit Page URL
[tabs]
======
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 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.
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
include-code::./MyController[tag=snippet,indent=0]
.Disabling the Default Submit Page
[tabs]
======
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 How to Generate and Consume One-Time Tokens
@@ -110,16 +424,157 @@ Some of the most common reasons to customize the `ReactiveOneTimeTokenService` a
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:
include-code::./OneTimeTokenServiceBeanSecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Passing the ReactiveOneTimeTokenService as a Bean
[tabs]
======
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.
include-code::./OneTimeTokenServiceDSLSecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Passing the ReactiveOneTimeTokenService using the DSL
[tabs]
======
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 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.
include-code::./SecurityConfig[tag=config,indent=0]
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::
+
[source,kotlin,role="secondary"]
----
@Bean
fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
----
======
@@ -53,12 +53,133 @@ 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.
In the following example, we are going to create a magic link and sent it to the user's email.
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.One-Time Token Login Configuration
[tabs]
======
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
<2> Create a login processing URL with the `token` as a query param
<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 `JavaMailSender` API to send the email to the user with the magic link
<5> Use the `RedirectOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
The email content will look similar to:
@@ -71,15 +192,70 @@ The default submit page will detect that the URL has the `token` query param and
== Changing the One-Time Token Generate URL
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 `tokenGeneratingUrl(String)` DSL method:
That URL can be changed by using the `generateTokenUrl(String)` DSL method:
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Changing the Generate URL
[tabs]
======
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]
====
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, `tokenGeneratingUrl("/api/ott/generate")`.
The URI passed to `generateTokenUrl` 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")`.
The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page.
====
@@ -89,17 +265,151 @@ 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 URL can also be changed, like so:
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Configuring the Default Submit Page URL
[tabs]
======
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 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.
include-code::./SecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
include-code::./MyController[tag=snippet,indent=0]
.Disabling the Default Submit Page
[tabs]
======
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 How to Generate and Consume One-Time Tokens
@@ -118,18 +428,161 @@ Some of the most common reasons to customize the `OneTimeTokenService` are, but
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:
include-code::./OneTimeTokenServiceBeanSecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Passing the OneTimeTokenService as a Bean
[tabs]
======
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.
include-code::./OneTimeTokenServiceDSLSecurityConfig[tag=config,indent=0]
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
.Passing the OneTimeTokenService using the DSL
[tabs]
======
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 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.
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));
};
}
----
include-code::./SecurityConfig[tag=config,indent=0]
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
return DefaultGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
----
======
+1 -1
View File
@@ -1,6 +1,6 @@
{
"dependencies": {
"antora": "3.2.0",
"antora": "3.2.0-rc.2",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
-1
View File
@@ -49,7 +49,6 @@ dependencies {
testImplementation 'org.springframework:spring-websocket'
testImplementation 'org.springframework:spring-webmvc'
testImplementation 'org.springframework:spring-context-support'
testImplementation 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'io.mockk:mockk'
testImplementation "org.junit.jupiter:junit-jupiter-api"
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,38 +0,0 @@
/*
* 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;
}
}
@@ -1,48 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
}
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,32 +0,0 @@
/*
* 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[]
@@ -1,46 +0,0 @@
/*
* 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[]
@@ -1,84 +0,0 @@
/*
* 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[]
@@ -1,41 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,37 +0,0 @@
/*
* 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;
}
}
@@ -1,47 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,39 +0,0 @@
/*
* 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[]
}
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,32 +0,0 @@
/*
* 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[]
@@ -1,46 +0,0 @@
/*
* 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[]
@@ -1,80 +0,0 @@
/*
* 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[]
@@ -1,41 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,41 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,41 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,36 +0,0 @@
/*
* 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")
}
}
@@ -1,46 +0,0 @@
/*
* 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[]
@@ -1,41 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
}
@@ -1,35 +0,0 @@
/*
* 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[]
@@ -1,32 +0,0 @@
/*
* 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[]
@@ -1,45 +0,0 @@
/*
* 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[]
@@ -1,78 +0,0 @@
/*
* 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[]
@@ -1,40 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,43 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,34 +0,0 @@
/*
* 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")
}
}
@@ -1,48 +0,0 @@
/*
* 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[]
@@ -1,44 +0,0 @@
/*
* 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[]
@@ -1,35 +0,0 @@
/*
* 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[]
}
@@ -1,34 +0,0 @@
/*
* 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[]
@@ -1,32 +0,0 @@
/*
* 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 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[]
@@ -1,47 +0,0 @@
/*
* 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 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 {
authorizeHttpRequests {
authorize("/my-ott-submit", authenticated)
authorize(anyRequest, authenticated)
}
formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
return http.build()
}
}
// end::config[]
@@ -1,69 +0,0 @@
/*
* 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.sendingtokentouser
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
// tag::snippet[]
@Component // <1>
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender) : OneTimeTokenGenerationSuccessHandler {
private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
val builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.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>
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: $magicLink"
this.mailSender.send(message) // <4>
this.redirectHandler.handle(request, response, oneTimeToken) // <5>
}
private fun getUserEmail(username: String): String {
/**/ return username
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
// end::snippet[]
@@ -1,40 +0,0 @@
/*
* 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.sendingtokentouser
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 { }
}
return http.build()
}
}
// end::config[]
+1 -1
View File
@@ -14,7 +14,7 @@
# limitations under the License.
#
springBootVersion=4.1.0-SNAPSHOT
version=7.1.2-SNAPSHOT
version=7.1.1
samplesBranch=main
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+6 -6
View File
@@ -18,8 +18,8 @@ org-springframework = "7.0.9"
com-password4j = "1.8.4"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.6.3"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.22.2"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.6.1"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.22.1"
com-google-inject-guice = "com.google.inject:guice:3.0"
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:10.9.1"
@@ -39,7 +39,7 @@ io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javafo
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.17"
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.15"
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:3.0.0"
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.2.0"
@@ -68,7 +68,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.4.7.Final"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.4.5.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-dokka-dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "org-jetbrains-dokka" }
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
@@ -89,7 +89,7 @@ org-springframework-data-spring-data-bom = "org.springframework.data:spring-data
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.1.1"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.2.2"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.2.1"
com-google-code-gson-gson = "com.google.code.gson:gson:2.14.0"
com-thaiopensource-trag = "com.thaiopensource:trang:20091111"
@@ -105,7 +105,7 @@ org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-inf
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.15'
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.14'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.9.RELEASE'
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
distributionSha256Sum=84fbba45c7f4c64abc77460e1c00f541e9f960e3c7ed2538f1ede19eacd873ae
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
@@ -24,12 +24,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
@@ -82,7 +79,7 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
.map((c) -> c.get(ServerWebExchange.class));
// @formatter:on
private ServerSecurityContextRepository serverSecurityContextRepository = new NonRotatingWebSessionServerSecurityContextRepository();
private ServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
@@ -144,7 +141,8 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
/**
* Sets a {@link ServerSecurityContextRepository} to use for refreshing a
* {@link SecurityContext}.
* {@link SecurityContext}, defaults to
* {@link WebSessionServerSecurityContextRepository}.
* @param serverSecurityContextRepository the {@link ServerSecurityContextRepository}
* to use
*/
@@ -318,28 +316,4 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
return this.serverSecurityContextRepository.save(exchange, securityContext);
}
private static final class NonRotatingWebSessionServerSecurityContextRepository
implements ServerSecurityContextRepository {
private static final Log logger = LogFactory.getLog(NonRotatingWebSessionServerSecurityContextRepository.class);
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
return Mono.empty();
}
@Override
public Mono<Void> save(ServerWebExchange exchange, @Nullable SecurityContext context) {
Assert.notNull(context, "context cannot be null");
// Save SecurityContext in WebSession without rotating session id.
return exchange.getSession().doOnNext((session) -> {
session.getAttributes()
.put(WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
logger.debug(LogMessage.format("Saved SecurityContext '%s' in WebSession: '%s'", context, session));
}).then();
}
}
}
@@ -50,7 +50,7 @@ public final class InMemoryOidcSessionRegistry implements OidcSessionRegistry {
}
@Override
public @Nullable OidcSessionInformation removeSessionInformation(String clientSessionId) {
public OidcSessionInformation removeSessionInformation(String clientSessionId) {
OidcSessionInformation information = this.sessions.remove(clientSessionId);
if (information != null) {
this.logger.trace("Removed client session");

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