1
0
mirror of synced 2026-07-08 12:20:02 +00:00

Compare commits

...

105 Commits

Author SHA1 Message Date
Steve Riesenberg 13e7c9f58d Release 5.5.3 2021-10-18 14:48:01 -05:00
Steve Riesenberg 14e6d8ad49 Revert "Update io.spring.javaformat to 0.0.29"
This reverts commit fafde0910f.
2021-10-18 14:23:51 -05:00
Steve Riesenberg d1a27676df Update org.springframework.data to 2021.0.6
Closes gh-10417
2021-10-18 14:20:16 -05:00
Steve Riesenberg 844879b84d Update org.springframework to 5.3.11
Closes gh-10416
2021-10-18 14:20:14 -05:00
Steve Riesenberg d5efbd34b5 Update org.jetbrains.kotlinx to 1.5.2
Closes gh-10415
2021-10-18 14:20:11 -05:00
Steve Riesenberg b9ef11d228 Update org.jetbrains.kotlin to 1.5.31
Closes gh-10414
2021-10-18 14:20:09 -05:00
Steve Riesenberg d9b1df8b65 Update org.eclipse.jetty to 9.4.44.v20210927
Closes gh-10413
2021-10-18 14:20:07 -05:00
Steve Riesenberg 020dd90566 Update io.spring.nohttp to 0.0.10
Closes gh-10412
2021-10-18 14:20:05 -05:00
Steve Riesenberg fafde0910f Update io.spring.javaformat to 0.0.29
Closes gh-10411
2021-10-18 14:20:02 -05:00
Steve Riesenberg b2db2bdb2a Update r2dbc-spi-test to 0.8.6.RELEASE
Closes gh-10410
2021-10-18 14:20:00 -05:00
Steve Riesenberg cce1c16555 Update io.projectreactor to 2020.0.12
Closes gh-10408
2021-10-18 14:19:56 -05:00
Steve Riesenberg e969c363c0 Update jackson-bom to 2.12.5
Closes gh-10405
2021-10-18 14:19:49 -05:00
Steve Riesenberg 764e696026 Update logback-classic to 1.2.6
Closes gh-10404
2021-10-18 14:19:48 -05:00
Josh Cummings 6e86fab19d Restructure SwitchUserFilter Logs
Issue gh-6311
2021-10-18 13:02:42 -05:00
Emil Sierżęga 1a3e80506c Fixed link in .editorconfig 2021-10-13 15:37:32 -06:00
Dávid Kováč eb0597154d Update JavaDoc according to implementation
Update ClaimAccessor#getClaimAsMap and ClaimAccessor#getClaimAsStringList
JavaDoc according to the current implementation

Closes gh-10117
2021-10-13 13:13:44 -06:00
Josh Cummings 97dfabe92e Polish SecurityNamespaceHandler Tests
Issue gh-8974
2021-10-13 11:37:06 -06:00
Emil Sierżęga 944463e19a SecurityNamespaceHandler: update schema version to 5.5
Closes gh-8974
2021-10-13 11:35:25 -06:00
Marcus Da Coregio 816e847af2 Allow SAML 2.0 loginProcessingURL without registrationId
Closes gh-10176
2021-10-05 12:54:39 -03:00
Josh Cummings 1f919bc791 Fix OAuth2 Error Code
Closes gh-10319
2021-09-28 14:55:37 -06:00
Rob Winch cae8990046 GitHub Actions uses spring-builds+github 2021-09-28 15:08:55 -05:00
heowc c9917b3cd0 Fix typo
Closes gh-10276
2021-09-22 16:35:32 -06:00
Anthony Lofton 1653b7848d Updated test.adoc SecurityMockServerConfigurers method references
Updated all references to SecurityMockServerConfigurers to refer to
correct methods.
Added documentation for mockJwt to include the
SecurityMockServerConfigurers class.

Issue gh-10254
2021-09-14 15:22:32 -03:00
Derek Van Blerkom c55f1f8bea Fix return type to allow further security config
Issue gh-10245
2021-09-13 15:41:04 -03:00
Marcus Da Coregio 670cf99258 Update docs to point to ACL samples
Closes gh-10110
2021-09-06 11:30:38 -03:00
Josh Cummings 41feec4766 Remove unused Sonar from Build
Closes gh-10205
2021-08-20 16:47:55 -06:00
Josh Cummings 34e04f642c Next Development Version 2021-08-16 12:48:56 -06:00
Josh Cummings bdc3feaa6d Release 5.5.2 2021-08-16 11:43:06 -06:00
Josh Cummings 134f8b0c5d Update org.springframework.data to 2021.0.4
Closes gh-10195
2021-08-16 10:02:27 -06:00
Josh Cummings 835ee559d7 Update org.springframework to 5.3.9
Closes gh-10194
2021-08-16 10:02:25 -06:00
Josh Cummings cfe48556d4 Update org.slf4j to 1.7.32
Closes gh-10193
2021-08-16 10:02:22 -06:00
Josh Cummings c7b73d7b3b Update org.jetbrains.kotlinx to 1.5.1
Closes gh-10192
2021-08-16 10:02:20 -06:00
Josh Cummings 618edc2e12 Update org.jetbrains.kotlin to 1.5.21
Closes gh-10191
2021-08-16 10:02:18 -06:00
Josh Cummings f7fef100c0 Update org.eclipse.jetty to 9.4.43.v20210629
Closes gh-10190
2021-08-16 10:02:15 -06:00
Josh Cummings 9be97e76dc Update org.aspectj to 1.9.7
Closes gh-10189
2021-08-16 10:02:13 -06:00
Josh Cummings 603e0a4140 Update io.projectreactor to 2020.0.10
Closes gh-10187
2021-08-16 10:02:08 -06:00
Josh Cummings b081627f52 Update com.nimbusds to 9.9.1
Closes gh-10186
2021-08-16 10:02:05 -06:00
Josh Cummings 10a0bda63f Update jackson-bom to 2.12.4
Closes gh-10183
2021-08-16 10:01:58 -06:00
Josh Cummings 7db5149163 Update logback-classic to 1.2.5
Closes gh-10182
2021-08-16 10:01:56 -06:00
Josh Cummings b206670245 Exclude Kotlin RC and M releases 2021-08-16 09:59:59 -06:00
Fabio Guenci b067aa4653 Preserve Null Claim Values
Prior to this commit ClaimTypeConverter returned the claims with the
original value for all the claims with a null converted value.
The changes allows ClaimTypeConverter to overwrite and return claims
with converted value of null.

Closes gh-10135
2021-08-16 08:22:31 -06:00
Abdul Al-Faraj ba16d91971 Improve OpenSAML Version Check
Closes gh-10077
2021-07-26 10:51:31 -06:00
Steve Riesenberg dfebd6d9d4 Revert "URL encode client credentials"
This reverts commit e6c268add0.

Issue gh-9610 gh-9858
Closes gh-10018
Closes gh-10121
2021-07-20 12:59:44 -05:00
Eleftheria Stein bd703ff4ac Fix release instructions for generating changelog 2021-07-19 13:42:35 +02:00
dmitrilc 7abaefda10 Update oauth2-resourceserver.adoc
fix the name of the parameter, from failure to badCredentials

Replaces AuthenticationFailureEvent

Remove AuthenticationFailureEvent Reference

Closes gh-10062
2021-07-16 11:58:34 -06:00
Marcus Da Coregio 4440020217 Update SAML docs to point to correct api url
Closes gh-9953
2021-07-12 15:47:28 -03:00
Marcus Da Coregio c2b9c0856d Use springFrameworkVersion property in docs links
Closes gh-9987
2021-07-12 15:47:09 -03:00
Marcus Da Coregio a06f47fac3 Add springFrameworkVersion property in gradle
Closes gh-9954
2021-07-12 15:45:01 -03:00
Marcus Da Coregio 8c0103498a Ensure line endings for .bat are not modified
Closes gh-10039
2021-07-12 15:37:29 -03:00
Luke Quinane e269e5cb91 Fix typos
Closes gh-10050
2021-07-08 09:20:50 -03:00
Eleftheria Stein ed8b08fa24 Fix README local Maven install command
Switching from the maven to maven-publish plugin means users should use publishToMavenLocal to install jars into their local Maven cache.

Closes gh-10047
2021-07-07 13:26:28 +02:00
Marcus Da Coregio 208e327629 Update docs links to point to minor version branches
Closes gh-9986
2021-07-01 09:08:53 -03:00
Marcus Da Coregio 51dfae1b91 Add samplesBranch property
Closes gh-10019
2021-07-01 09:08:30 -03:00
Daniel Garnier-Moiroux 2fd94752c9 Fix Saml2WebSsoAuthenticationRequestFilter javadoc
Closes gh-10025
2021-06-30 13:51:29 +02:00
/usr/local/ΕΨΗΕΛΩΝ 61284ce22d Improve AuthenticationManagerBeanDefinitionParser XML parsing
Closes gh-7282
2021-06-28 11:55:05 +02:00
Eleftheria Stein fdd017d935 Apply DefaultLoginPageConfigurer before logout
If they are not applied in this order, then the LogoutConfigurer cannot
set the logoutSuccessUrl, because the DefaultLoginPageGeneratingFilter
does not exist yet.
This impacts users that inject the default HttpSecurity bean.

Closes gh-9973
2021-06-24 10:28:19 +02:00
Josh Cummings bfeb6bd756 Next Development Version 2021-06-21 12:59:19 -06:00
Josh Cummings e41360b4a3 Release 5.5.1 2021-06-21 11:00:32 -06:00
Josh Cummings e2e06539fc Remove commons-codec constraint
Closes gh-9977
2021-06-21 10:41:22 -06:00
Josh Cummings 93f59a228b Update to OpenSAML 4.1.1
Closes gh-9976
2021-06-21 10:41:11 -06:00
Josh Cummings b2bb014492 Update to nimbus-jose-jwt 9.10
Closes gh-9975
2021-06-21 10:41:06 -06:00
Josh Cummings 062910c2c4 Update to oauth2-oidc-sdk 9.9
Closes gh-9974
2021-06-21 10:40:59 -06:00
Josh Cummings 40fdb5a60d Update org.springframework to 5.3.8
Closes gh-9984
2021-06-21 10:40:06 -06:00
Josh Cummings 2fa0da5cde Update org.slf4j to 1.7.31
Closes gh-9983
2021-06-21 10:40:06 -06:00
Josh Cummings c5f4ae5c0a Update org.jetbrains.kotlin to 1.5.10
Closes gh-9982
2021-06-21 10:39:54 -06:00
Josh Cummings c68c1c55ff Update hibernate-entitymanager to 5.4.32.Final
Closes gh-9981
2021-06-21 10:37:30 -06:00
Josh Cummings be2595bc66 Update org.eclipse.jetty to 9.4.42.v20210604
Closes gh-9980
2021-06-21 10:37:28 -06:00
Josh Cummings dddd71439f Update io.rsocket to 1.1.1
Closes gh-9979
2021-06-21 10:37:25 -06:00
Marcus Hert da Coregio 95a1ccb52c Fix Broken Links in Docs
Closes gh-9869
2021-06-21 10:58:44 -03:00
Marcus Hert da Coregio 4060f83261 Add a link checker to docs build
Closes gh-9818
2021-06-21 10:58:44 -03:00
Marcus Da Coregio f9a7ae0a6f Update links to point to migrated samples
Closes gh-9816
2021-06-21 10:58:43 -03:00
Marcus Hert da Coregio 425d008f1d Add messaging to documentation about sample migration
Closes gh-9815
2021-06-21 10:58:43 -03:00
Eleftheria Stein b73104c9d1 Add missing WebFlux CORS Reference
Closes gh-9951
2021-06-18 16:33:01 +02:00
Eleftheria Stein 895ef6cf76 Remove non-existent class from RSocket reference docs
Closes gh-9949
2021-06-18 14:18:31 +02:00
Eleftheria Stein cb4bb463da Disable default logout page when logout disabled
Closes gh-9475
2021-06-17 17:09:34 +02:00
Arnaud Mergey 5fd81eeaf1 fix typo preventing full exception to be displayed in log
closes gh-9901
2021-06-17 08:38:24 -06:00
Steve Riesenberg a332e2a728 Support additional client authentication methods
Closes gh-9780
2021-06-16 16:03:13 -05:00
Steve Riesenberg 9daf058a6e Handle missing authorization endpoint uri
Closes gh-9795
2021-06-16 16:00:53 -05:00
Steve Riesenberg 839cc5e851 Remove validation for unsupported grant types
Closes gh-9828
2021-06-16 15:55:45 -05:00
Christian Frommeyer 0087128be8 Replace StringUtils from oauth2-oidc-sdk
SecurityMockServerConfigurers.java previously used the StringUtils from
the optional oauth2-oidc-sdk dependency. Replacing this with the
StringUtils from the spring framework this should not force adding the
dependency in cases where it is not actually used.

Closes gh-9923
2021-06-16 13:22:49 -06:00
Josh Cummings bfc9c5e18f Use stack-workflow-status Action
Closes gh-9792
2021-06-16 13:21:20 -06:00
Joe Grandja 6fbd038111 Jwt client authentication converter detects new key
Closes gh-9814
2021-06-16 12:58:01 -04:00
Steve Riesenberg 700bda68b7 Store one request by default in WebSessionOAuth2ServerAuthorizationRequestRepository
Related to gh-9649
Closes gh-9857
2021-06-15 11:32:35 -05:00
Marcus Hert da Coregio 53870ab3de Fix Adding Filter Relative to Custom Filter
Closes gh-9787
2021-06-14 15:51:27 -03:00
Josh Cummings fe13b488a1 Anonymous Authentication Argument Resolution Docs
Closes gh-3338
2021-06-08 16:12:38 -06:00
Josh Cummings c38495bfaf Change Getting Started Link
Point to 5.5.x reference guide

Issue gh-6502
2021-06-08 13:56:58 -06:00
Josh Cummings c4b210b580 Fix Getting Started Link
Closes gh-6502
2021-06-08 13:44:06 -06:00
Josh Cummings 1a28b4f849 Format PayloadInterceptorRSocket
Issue gh-9345
2021-06-04 12:47:25 -06:00
Josh Cummings 63cd52d1a6 PayloadInterceptorRSocket retains all payloads
Flux#skip discards its corresponding elements, meaning that they
aren't intended for reuse. When using RSocket's ByteBufPayloads,
this means that the bytes are releaseed back into RSocket's pool.

Since the downstream request may still need the skipped payload,
we should construct the publisher in a different way so as to
avoid the preemptive release.

Deferring Spring JavaFormat to clarify what changed.

Closes gh-9345
2021-06-04 12:47:22 -06:00
Eleftheria Stein 895ae0ac55 Remove hard dependency on Kotlin
Closes gh-9811
2021-06-03 18:34:52 +03:00
Steve Riesenberg e6c268add0 URL encode client credentials
Closes gh-9610
2021-06-03 09:12:18 -05:00
Eleftheria Stein 82d471a592 Fix Resource Server clock skew default value in docs
Closes gh-6611
2021-06-02 12:22:42 +03:00
Josh Cummings f48a006034 Polish postLogoutRedirectUri encoding
Issue gh-9511
2021-05-26 13:51:26 -06:00
Hans Hosea Schaefer b7a0959ede Encode postLogoutRedirectUri query params
Now encodes already encoded queryparameters in postLogoutRedirectUrl
correctly

Closes gh-9511
2021-05-26 13:51:15 -06:00
Marcus Hert da Coregio 29f4193529 Adjust createNewSessionIfAllowed to prevent NPE
Ensure that isTransientAuthentication reuses the same authentication object from saveContext

Closes gh-8947
2021-05-26 13:46:08 -03:00
Steve Riesenberg ab59adbfd2 Handle encoded spaces in the root dn
Fixes an issue where provider URLs passed to the constructor of the
DefaultSpringSecurityContextSource can be URL encoded, resulting in
an invalid base dn. Additionally adds support for list constructor
to support spaces in base dn.

Closes gh-9742
2021-05-26 10:29:49 -05:00
Steve Riesenberg de4b3a4310 Handle custom status codes in error handler
Fixes an issue where custom status codes in the error response cause an
IllegalArgumentException to be thrown when resolving an HttpStatus.

Closes gh-9741
2021-05-25 13:41:04 -05:00
Marcus Hert da Coregio e691906dbc Combine multiple OS builds into one
Closes gh-9790
2021-05-25 14:19:46 -03:00
Marcus Hert da Coregio 5a06fbd984 Update Env Variables in Build Windows Job
Closes gh-9761
2021-05-25 11:07:37 -06:00
Eleftheria Stein 34b55242fe Ensure Kotlin 1.3 compatibility
Closes gh-9765
2021-05-19 10:15:24 +02:00
Rob Winch 26e34368c0 Use GPG_PRIVATE_KEY directly
Closes gh-9776
2021-05-18 17:19:24 -05:00
Rob Winch 0fb7c24cbb core depends on crypto
Issue gh-9767
2021-05-18 17:19:24 -05:00
Josh Cummings 0351883dfa Include all configured branches in CI
Closes gh-9775
2021-05-18 15:59:40 -06:00
Rob Winch 95132d9fe3 Remove spring-security-crypto from spring-core pom
Instead of having api extend included configuration, we should use the
*Classpath configurations.

Closes gh-9767
2021-05-18 15:32:17 -05:00
Rob Winch bf9f1de6e1 Artifactory defaults to publish mavenJava Publication
Closes gh-9772
2021-05-18 15:32:17 -05:00
109 changed files with 1863 additions and 666 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# EditorConfig for Spring Security
# see https://github.com/spring-projects/spring-security/blob/master/CONTRIBUTING.md#mind-the-whitespace
# see https://github.com/spring-projects/spring-security/blob/master/CONTRIBUTING.adoc#mind-the-whitespace
root = true
+3
View File
@@ -1,6 +1,9 @@
# Normalize line endings to auto.
* text auto
# Ensure that line endings for DOS batch files are not modified.
*.bat -text
# Ensure the following are treated as binary.
*.cer binary
*.graffle binary
@@ -2,8 +2,6 @@ name: CI
on:
push:
branches:
- main
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
@@ -26,16 +24,6 @@ jobs:
outputs:
runjobs: ${{ steps.continue.outputs.runjobs }}
steps:
- uses: actions/checkout@v2
- name: Initiate error tracking
uses: spring-projects/track-build-errors-action@v1
with:
job-name: "prerequisites"
- name: Export errors file
uses: actions/upload-artifact@v2
with:
name: errors
path: job-prerequisites.txt
- id: continue
name: Determine if should continue
if: env.RUN_JOBS == 'true'
@@ -43,7 +31,10 @@ jobs:
build_jdk_11:
name: Build JDK 11
needs: [prerequisites]
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v2
@@ -54,66 +45,18 @@ jobs:
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Build with Gradle
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
build_windows:
name: Build Windows
needs: [prerequisites]
runs-on: windows-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Build with Gradle
run: |
set GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
set GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
set GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
env:
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
run: ./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
snapshot_tests:
name: Test against snapshots
needs: [prerequisites]
@@ -128,71 +71,16 @@ jobs:
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
- name: Snapshot Tests
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion='20+' -PspringDataVersion='Neumann-BUILD-SNAPSHOT' -PrsocketVersion=1.1.0-SNAPSHOT -PspringBootVersion=2.4.0-SNAPSHOT -PlocksDisabled --stacktrace
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
sonar_analysis:
name: Static Code Analysis
needs: [prerequisites]
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
env:
SONAR_URL: ${{ secrets.SONAR_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
- name: Run Sonar on given (non-main) branch
if: ${{ github.ref != 'refs/heads/main' }}
run: |
export BRANCH=${GITHUB_REF#refs/heads/}
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew sonarqube -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PexcludeProjects='**/samples/**' -Dsonar.projectKey="spring-security-${GITHUB_REF#refs/heads/}" -Dsonar.projectName="spring-security-${GITHUB_REF#refs/heads/}" -Dsonar.host.url="$SONAR_URL" -Dsonar.login="$SONAR_TOKEN" --stacktrace
- name: Run Sonar on main
if: ${{ github.ref == 'refs/heads/main' }}
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew sonarqube -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PexcludeProjects='**/samples/**' -Dsonar.host.url="$SONAR_URL" -Dsonar.login="$SONAR_TOKEN" --stacktrace
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
deploy_artifacts:
name: Deploy Artifacts
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis]
needs: [build_jdk_11, snapshot_tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -203,37 +91,23 @@ jobs:
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
- name: Deploy artifacts
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
export VERSION_HEADER=$'Version: GnuPG v2\n\n'
export ORG_GRADLE_PROJECT_signingKey=${GPG_PRIVATE_KEY_NO_HEADER#"$VERSION_HEADER"}
export ORG_GRADLE_PROJECT_signingPassword="$GPG_PASSPHRASE"
./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
deploy_docs:
name: Deploy Docs
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis]
needs: [build_jdk_11, snapshot_tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -244,7 +118,7 @@ jobs:
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
- name: Deploy Docs
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -255,20 +129,9 @@ jobs:
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_HOST: ${{ secrets.DOCS_HOST }}
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
deploy_schema:
name: Deploy Schema
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis]
needs: [build_jdk_11, snapshot_tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -279,7 +142,7 @@ jobs:
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds' >> ~/.gradle/gradle.properties
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
- name: Deploy Schema
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -290,34 +153,16 @@ jobs:
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_HOST: ${{ secrets.DOCS_HOST }}
- name: Track error step
uses: spring-projects/track-build-errors-action@v1
if: ${{ failure() }}
with:
job-name: ${{ github.job }}
- name: Export errors file
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: errors
path: job-${{ github.job }}.txt
notify_result:
name: Check for failures
needs: [build_jdk_11, build_windows, snapshot_tests, sonar_analysis, deploy_artifacts, deploy_docs, deploy_schema]
if: always()
needs: [build_jdk_11, snapshot_tests, deploy_artifacts, deploy_docs, deploy_schema]
if: failure()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download errors folder
uses: actions/download-artifact@v2
with:
name: errors
- name: Send Slack message
uses: spring-projects/notify-slack-errors-action@v1
uses: Gamesight/slack-workflow-status@v1.0.1
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
branch-name: ${{ github.ref }}
commit-sha: ${{ github.sha }}
commit-owner: ${{ github.actor }}
repo-name: ${{ github.repository }}
run-id: ${{ github.run_id }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
+2 -2
View File
@@ -22,7 +22,7 @@ Be sure to read the https://docs.spring.io/spring-security/site/docs/current/ref
Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
== Quick Start
We recommend you visit https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/[Spring Security Reference] and read the "Getting Started" page.
See https://docs.spring.io/spring-security/site/docs/5.5.x/reference/html5/#servlet-hello[Hello Spring Security] to get started with a "Hello, World" application.
== Building from Source
Spring Security uses a https://gradle.org[Gradle]-based build system.
@@ -43,7 +43,7 @@ git clone git@github.com:spring-projects/spring-security.git
=== Install all spring-\* jars into your local Maven cache
[indent=0]
----
./gradlew install
./gradlew publishToMavenLocal
----
=== Compile and test; build all jars, distribution zips, and docs
+1 -1
View File
@@ -143,7 +143,7 @@ Generate the Release Notes replacing:
* <next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
----
$ ./gradlew generateChangelog -P<next-version>
$ ./gradlew generateChangelog -PnextVersion=<next-version>
----
* Copy the release notes to your clipboard (your mileage may vary with
+9 -7
View File
@@ -1,7 +1,7 @@
buildscript {
dependencies {
classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion"
classpath 'io.spring.nohttp:nohttp-gradle:0.0.8'
classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
classpath "io.freefair.gradle:aspectj-plugin:5.3.3.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.netflix.nebula:nebula-project-plugin:8.0.0"
@@ -68,12 +68,6 @@ updateDependenciesSettings {
milestoneVersions()
snapshotVersions()
addRule { components ->
components.withModule("commons-codec:commons-codec") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("commons-codec updates break saml tests");
}
}
components.withModule("org.python:jython") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
@@ -93,6 +87,14 @@ updateDependenciesSettings {
if (candidate.getGroup().equals("org.opensaml")) {
selection.reject("org.opensaml maintains two different versions, so it must be updated manually");
}
if (candidate.getGroup().equals("org.jetbrains.kotlin")) {
if (candidate.getVersion().endsWith("-RC")) {
selection.reject("On a maintenance branch, we should not take RC versions")
}
if (candidate.getVersion().contains("-M")) {
selection.reject("On a maintenance branch, we should not take milestone versions")
}
}
}
}
}
+3 -2
View File
@@ -72,14 +72,15 @@ dependencies {
implementation localGroovy()
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
implementation 'io.projectreactor:reactor-core:3.4.6'
implementation 'io.projectreactor:reactor-core:3.4.11'
implementation 'gradle.plugin.org.gretty:gretty:3.0.1'
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
implementation 'io.spring.gradle:docbook-reference-plugin:0.3.1'
implementation 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
implementation 'io.spring.nohttp:nohttp-gradle:0.0.8'
implementation 'io.spring.nohttp:nohttp-gradle:0.0.10'
implementation 'org.aim42:htmlSanityCheck:1.1.6'
implementation 'org.asciidoctor:asciidoctor-gradle-jvm:3.1.0'
implementation 'org.asciidoctor:asciidoctor-gradle-jvm-pdf:3.1.0'
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
@@ -36,6 +36,9 @@ class ArtifactoryPlugin implements Plugin<Project> {
password = artifactoryPassword
}
}
defaults {
publications('mavenJava')
}
}
}
}
@@ -36,7 +36,7 @@ class CheckstylePlugin implements Plugin<Project> {
if (checkstyleDir.exists() && checkstyleDir.directory) {
project.getPluginManager().apply('checkstyle')
project.dependencies.add('checkstyle', 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.15')
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.8')
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.10')
project.checkstyle {
configDirectory = checkstyleDir
@@ -1,14 +1,13 @@
package io.spring.gradle.convention
import org.aim42.htmlsanitycheck.HtmlSanityCheckPlugin
import org.aim42.htmlsanitycheck.HtmlSanityCheckTask
import org.aim42.htmlsanitycheck.check.BrokenHttpLinksChecker
import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.DependencySet
import org.gradle.api.plugins.PluginManager
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.bundling.Zip
/**
@@ -25,6 +24,7 @@ public class DocsPlugin implements Plugin<Project> {
pluginManager.apply(AsciidoctorConventionPlugin);
pluginManager.apply(DeployDocsPlugin);
pluginManager.apply(JavadocApiPlugin);
pluginManager.apply(HtmlSanityCheckPlugin)
String projectName = Utils.getProjectName(project);
String pdfFilename = projectName + "-reference.pdf";
@@ -38,6 +38,17 @@ public class DocsPlugin implements Plugin<Project> {
}
}
project.tasks.withType(HtmlSanityCheckTask) { HtmlSanityCheckTask t ->
project.configure(t) {
t.dependsOn 'asciidoctor'
t.checkerClasses = [BrokenHttpLinksChecker]
t.checkingResultsDir = new File(project.getBuildDir(), "/report/htmlchecks")
t.failOnErrors = false
t.httpConnectionTimeout = 3000
t.sourceDir = new File(project.getBuildDir(), "/docs/asciidoc/")
t.sourceDocuments = project.files(new File(project.getBuildDir(), "/docs/asciidoc/index.html"))
}
}
Task docsZip = project.tasks.create('docsZip', Zip) {
dependsOn 'api', 'asciidoctor'
+2 -2
View File
@@ -100,10 +100,10 @@ rncToXsd {
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
apiVersion = "1.4"
languageVersion = "1.3"
apiVersion = "1.3"
freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings"]
jvmTarget = "1.8"
languageVersion = "1.4"
}
}
@@ -93,7 +93,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
if (!namespaceMatchesVersion(element)) {
pc.getReaderContext().fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or "
+ "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema "
+ "with Spring Security 5.4. Please update your schema declarations to the 5.4 schema.", element);
+ "with Spring Security 5.5. Please update your schema declarations to the 5.5 schema.", element);
}
String name = pc.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(name);
@@ -213,7 +213,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
private boolean matchesVersionInternal(Element element) {
String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation");
return schemaLocation.matches("(?m).*spring-security-5\\.4.*.xsd.*")
return schemaLocation.matches("(?m).*spring-security-5\\.5.*.xsd.*")
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|| !schemaLocation.matches("(?m).*spring-security.*");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -114,8 +114,18 @@ final class FilterOrderRegistration {
put(SwitchUserFilter.class, order.next());
}
private void put(Class<? extends Filter> filter, int position) {
/**
* Register a {@link Filter} with its specific position. If the {@link Filter} was
* already registered before, the position previously defined is not going to be
* overriden
* @param filter the {@link Filter} to register
* @param position the position to associate with the {@link Filter}
*/
void put(Class<? extends Filter> filter, int position) {
String className = filter.getName();
if (this.filterToOrder.containsKey(className)) {
return;
}
this.filterToOrder.put(className, position);
}
@@ -2653,6 +2653,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
int order = this.filterOrders.getOrder(registeredFilter) + offset;
this.filters.add(new OrderedFilter(filter, order));
this.filterOrders.put(filter.getClass(), order);
return this;
}
@@ -94,8 +94,8 @@ class HttpSecurityConfiguration {
.requestCache(withDefaults())
.anonymous(withDefaults())
.servletApi(withDefaults())
.logout(withDefaults())
.apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
return http;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2021 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.
@@ -97,7 +97,10 @@ public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
if (this.loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) {
this.loginPageGeneratingFilter = postProcess(this.loginPageGeneratingFilter);
http.addFilter(this.loginPageGeneratingFilter);
http.addFilter(this.logoutPageGeneratingFilter);
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null) {
http.addFilter(this.logoutPageGeneratingFilter);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -160,7 +160,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
* @param repo the repository of relying parties
* @return the {@link Saml2LoginConfigurer} for further configuration
*/
public Saml2LoginConfigurer relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) {
public Saml2LoginConfigurer<B> relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) {
this.relyingPartyRegistrationRepository = repo;
return this;
}
@@ -172,10 +172,19 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return this;
}
/**
* Specifies the URL to validate the credentials. If specified a custom URL, consider
* specifying a custom {@link AuthenticationConverter} via
* {@link #authenticationConverter(AuthenticationConverter)}, since the default
* {@link AuthenticationConverter} implementation relies on the
* <code>{registrationId}</code> path variable to be present in the URL
* @param loginProcessingUrl the URL to validate the credentials
* @return the {@link Saml2LoginConfigurer} for additional customization
* @see Saml2WebSsoAuthenticationFilter#DEFAULT_FILTER_PROCESSES_URI
*/
@Override
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
Assert.state(loginProcessingUrl.contains("{registrationId}"), "{registrationId} path variable is required");
this.loginProcessingUrl = loginProcessingUrl;
return this;
}
@@ -254,14 +263,25 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
private AuthenticationConverter getAuthenticationConverter(B http) {
if (this.authenticationConverter == null) {
Assert.state(this.loginProcessingUrl.contains("{registrationId}"),
"loginProcessingUrl must contain {registrationId} path variable");
return new Saml2AuthenticationTokenConverter(
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository));
}
return this.authenticationConverter;
}
private String version() {
String version = Version.getVersion();
if (version != null) {
return version;
}
return Version.class.getModule().getDescriptor().version().map(Object::toString)
.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
}
private void registerDefaultAuthenticationProvider(B http) {
if (Version.getVersion().startsWith("4")) {
if (version().startsWith("4")) {
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
}
else {
@@ -346,7 +366,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
private Saml2AuthenticationRequestFactory getResolver(B http) {
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
if (resolver == null) {
if (Version.getVersion().startsWith("4")) {
if (version().startsWith("4")) {
return new OpenSaml4AuthenticationRequestFactory();
}
return new OpenSamlAuthenticationRequestFactory();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 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.
@@ -57,6 +57,8 @@ public class AuthenticationManagerBeanDefinitionParser implements BeanDefinition
private static final String ATT_ERASE_CREDENTIALS = "erase-credentials";
private static final String AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME = "defaultAuthenticationEventPublisher";
@Override
public BeanDefinition parse(Element element, ParserContext pc) {
String id = element.getAttribute("id");
@@ -86,11 +88,15 @@ public class AuthenticationManagerBeanDefinitionParser implements BeanDefinition
if ("false".equals(element.getAttribute(ATT_ERASE_CREDENTIALS))) {
providerManagerBldr.addPropertyValue("eraseCredentialsAfterAuthentication", false);
}
// Add the default event publisher
BeanDefinition publisher = new RootBeanDefinition(DefaultAuthenticationEventPublisher.class);
String pubId = pc.getReaderContext().generateBeanName(publisher);
pc.registerBeanComponent(new BeanComponentDefinition(publisher, pubId));
providerManagerBldr.addPropertyReference("authenticationEventPublisher", pubId);
if (!pc.getRegistry().containsBeanDefinition(AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME)) {
// Add the default event publisher to the context
BeanDefinition publisher = new RootBeanDefinition(DefaultAuthenticationEventPublisher.class);
pc.registerBeanComponent(new BeanComponentDefinition(publisher, AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME));
}
providerManagerBldr.addPropertyReference("authenticationEventPublisher",
AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME);
pc.registerBeanComponent(new BeanComponentDefinition(providerManagerBldr.getBeanDefinition(), id));
if (StringUtils.hasText(alias)) {
pc.getRegistry().registerAlias(id, alias);
@@ -2223,7 +2223,10 @@ public class ServerHttpSecurity {
}
if (loginPage != null) {
http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
http.addFilterAt(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
if (http.logout != null) {
http.addFilterAt(new LogoutPageGeneratingWebFilter(),
SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.messaging.Message;
import org.springframework.security.config.util.InMemoryXmlApplicationContext;
import org.springframework.security.config.util.SpringSecurityVersions;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
@@ -147,4 +148,11 @@ public class SecurityNamespaceHandlerTests {
// should load just fine since no websocket block
}
@Test
public void configureWhenOldVersionThenErrorMessageContainsCorrectVersion() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> new InMemoryXmlApplicationContext(XML_AUTHENTICATION_MANAGER, "3.0", null))
.withMessageContaining(SpringSecurityVersions.getCurrentXsdVersionFromSpringSchemas());
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.builders;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.junit.Test;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import static org.assertj.core.api.Assertions.assertThat;
public class FilterOrderRegistrationTests {
private final FilterOrderRegistration filterOrderRegistration = new FilterOrderRegistration();
@Test
public void putWhenNewFilterThenInsertCorrect() {
int position = 153;
this.filterOrderRegistration.put(MyFilter.class, position);
Integer order = this.filterOrderRegistration.getOrder(MyFilter.class);
assertThat(order).isEqualTo(position);
}
@Test
public void putWhenCustomFilterAlreadyExistsThenDoesNotOverride() {
int position = 160;
this.filterOrderRegistration.put(MyFilter.class, position);
this.filterOrderRegistration.put(MyFilter.class, 173);
Integer order = this.filterOrderRegistration.getOrder(MyFilter.class);
assertThat(order).isEqualTo(position);
}
@Test
public void putWhenPredefinedFilterThenDoesNotOverride() {
int position = 100;
Integer predefinedFilterOrderBefore = this.filterOrderRegistration.getOrder(ChannelProcessingFilter.class);
this.filterOrderRegistration.put(MyFilter.class, position);
Integer myFilterOrder = this.filterOrderRegistration.getOrder(MyFilter.class);
Integer predefinedFilterOrderAfter = this.filterOrderRegistration.getOrder(ChannelProcessingFilter.class);
assertThat(myFilterOrder).isEqualTo(position);
assertThat(predefinedFilterOrderAfter).isEqualTo(predefinedFilterOrderBefore).isEqualTo(position);
}
static class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -30,14 +30,18 @@ import org.assertj.core.api.ListAssert;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import static org.assertj.core.api.Assertions.assertThat;
@@ -70,6 +74,46 @@ public class HttpSecurityAddFilterTest {
ExceptionTranslationFilter.class);
}
@Test
public void addFilterAfterWhenAfterCustomFilterThenOrderCorrect() {
this.spring.register(MyOtherFilterRelativeToMyFilterAfterConfig.class).autowire();
assertThatFilters().containsSubsequence(WebAsyncManagerIntegrationFilter.class, MyFilter.class,
MyOtherFilter.class);
}
@Test
public void addFilterBeforeWhenBeforeCustomFilterThenOrderCorrect() {
this.spring.register(MyOtherFilterRelativeToMyFilterBeforeConfig.class).autowire();
assertThatFilters().containsSubsequence(MyOtherFilter.class, MyFilter.class,
WebAsyncManagerIntegrationFilter.class);
}
@Test
public void addFilterAtWhenAtCustomFilterThenOrderCorrect() {
this.spring.register(MyOtherFilterRelativeToMyFilterAtConfig.class).autowire();
assertThatFilters().containsSubsequence(WebAsyncManagerIntegrationFilter.class, MyFilter.class,
MyOtherFilter.class, SecurityContextPersistenceFilter.class);
}
@Test
public void addFilterBeforeWhenCustomFilterDifferentPlacesThenOrderCorrect() {
this.spring.register(MyOtherFilterBeforeToMyFilterMultipleAfterConfig.class).autowire();
assertThatFilters().containsSubsequence(WebAsyncManagerIntegrationFilter.class, MyOtherFilter.class,
MyFilter.class, ExceptionTranslationFilter.class);
}
@Test
public void addFilterBeforeAndAfterWhenCustomFiltersDifferentPlacesThenOrderCorrect() {
this.spring.register(MyAnotherFilterRelativeToMyCustomFiltersMultipleConfig.class).autowire();
assertThatFilters().containsSubsequence(HeaderWriterFilter.class, MyFilter.class, MyOtherFilter.class,
MyOtherFilter.class, MyAnotherFilter.class, MyFilter.class, ExceptionTranslationFilter.class);
}
private ListAssert<Class<?>> assertThatFilters() {
FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class);
List<Class<?>> filters = filterChain.getFilters("/").stream().map(Object::getClass)
@@ -87,6 +131,26 @@ public class HttpSecurityAddFilterTest {
}
static class MyOtherFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}
}
static class MyAnotherFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@EnableWebSecurity
static class MyFilterMultipleAfterConfig extends WebSecurityConfigurerAdapter {
@@ -129,4 +193,83 @@ public class HttpSecurityAddFilterTest {
}
@EnableWebSecurity
static class MyOtherFilterRelativeToMyFilterAfterConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterAfter(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
.addFilterAfter(new MyOtherFilter(), MyFilter.class);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
static class MyOtherFilterRelativeToMyFilterBeforeConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterBefore(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
static class MyOtherFilterRelativeToMyFilterAtConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterAt(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
.addFilterAt(new MyOtherFilter(), MyFilter.class);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
static class MyOtherFilterBeforeToMyFilterMultipleAfterConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterAfter(new MyFilter(), WebAsyncManagerIntegrationFilter.class)
.addFilterAfter(new MyFilter(), ExceptionTranslationFilter.class)
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
static class MyAnotherFilterRelativeToMyCustomFiltersMultipleConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterAfter(new MyFilter(), HeaderWriterFilter.class)
.addFilterBefore(new MyOtherFilter(), ExceptionTranslationFilter.class)
.addFilterAfter(new MyOtherFilter(), MyFilter.class)
.addFilterAt(new MyAnotherFilter(), MyOtherFilter.class)
.addFilterAfter(new MyFilter(), MyAnotherFilter.class);
// @formatter:on
return http.build();
}
}
}
@@ -187,6 +187,18 @@ public class HttpSecurityConfigurationTests {
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
}
@Test
public void loginWhenUsingDefaultsThenDefaultLoginFailurePageGenerated() throws Exception {
this.spring.register(SecurityEnabledConfig.class).autowire();
this.mockMvc.perform(get("/login?error")).andExpect(status().isOk());
}
@Test
public void loginWhenUsingDefaultsThenDefaultLogoutSuccessPageGenerated() throws Exception {
this.spring.register(SecurityEnabledConfig.class).autowire();
this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk());
}
@RestController
static class NameController {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -46,11 +46,14 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link DefaultLoginPageConfigurer}
@@ -375,6 +378,18 @@ public class DefaultLoginPageConfigurerTests {
.isZero();
}
@Test
public void formLoginWhenLogoutEnabledThenCreatesDefaultLogoutPage() throws Exception {
this.spring.register(DefaultLogoutPageConfig.class).autowire();
this.mvc.perform(get("/logout").with(user("user"))).andExpect(status().isOk());
}
@Test
public void formLoginWhenLogoutDisabledThenDefaultLogoutPageDoesNotExist() throws Exception {
this.spring.register(LogoutDisabledConfig.class).autowire();
this.mvc.perform(get("/logout").with(user("user"))).andExpect(status().isNotFound());
}
@EnableWebSecurity
static class DefaultLoginPageConfig extends WebSecurityConfigurerAdapter {
@@ -533,6 +548,41 @@ public class DefaultLoginPageConfigurerTests {
}
@EnableWebSecurity
static class DefaultLogoutPageConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(withDefaults());
// @formatter:on
}
}
@EnableWebSecurity
static class LogoutDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.logout((logout) -> logout
.disable()
);
// @formatter:on
}
}
static class ReflectingObjectPostProcessor implements ObjectPostProcessor<Object> {
@Override
@@ -37,6 +37,7 @@ import org.mockito.ArgumentCaptor;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -49,6 +50,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -77,7 +79,9 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.context.HttpRequestResponseHolder;
@@ -91,6 +95,7 @@ import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
@@ -117,6 +122,8 @@ public class Saml2LoginConfigurerTests {
private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=";
private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class);
@Autowired
private ConfigurableApplicationContext context;
@@ -230,6 +237,33 @@ public class Saml2LoginConfigurerTests {
assertThat(exception.getCause()).isInstanceOf(IOException.class);
}
@Test
public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class)
.autowire())
.havingRootCause().isInstanceOf(IllegalStateException.class)
.withMessage("loginProcessingUrl must contain {registrationId} path variable");
}
@Test
public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate()
throws Exception {
this.spring.register(CustomLoginProcessingUrlCustomAuthenticationConverter.class).autowire();
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
.assertingPartyDetails((party) -> party.verificationX509Credentials(
(c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
.build();
String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE));
given(AUTHENTICATION_CONVERTER.convert(any(HttpServletRequest.class)))
.willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response));
// @formatter:off
MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE);
// @formatter:on
this.mvc.perform(request).andExpect(redirectedUrl("/"));
verify(AUTHENTICATION_CONVERTER).convert(any(HttpServletRequest.class));
}
private void validateSaml2WebSsoAuthenticationFilterConfiguration() {
// get the OpenSamlAuthenticationProvider
Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain);
@@ -337,10 +371,10 @@ public class Saml2LoginConfigurerTests {
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests((authz) -> authz
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
.authorizeRequests((authz) -> authz
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
// @formatter:on
}
@@ -359,11 +393,11 @@ public class Saml2LoginConfigurerTests {
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests((authz) -> authz
.anyRequest().authenticated()
)
.saml2Login((saml2) -> {
});
.authorizeRequests((authz) -> authz
.anyRequest().authenticated()
)
.saml2Login((saml2) -> {
});
// @formatter:on
}
@@ -395,6 +429,62 @@ public class Saml2LoginConfigurerTests {
}
@EnableWebSecurity
@Import(Saml2LoginConfigBeans.class)
static class CustomAuthenticationConverterBean {
private final Saml2AuthenticationTokenConverter authenticationConverter = mock(
Saml2AuthenticationTokenConverter.class);
@Bean
SecurityFilterChain app(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz.anyRequest().authenticated())
.saml2Login(Customizer.withDefaults());
return http.build();
}
@Bean
Saml2AuthenticationTokenConverter authenticationConverter() {
return this.authenticationConverter;
}
}
@EnableWebSecurity
@Import(Saml2LoginConfigBeans.class)
static class CustomLoginProcessingUrlDefaultAuthenticationConverter {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.saml2Login((saml2) -> saml2.loginProcessingUrl("/my/custom/url"));
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
@Import(Saml2LoginConfigBeans.class)
static class CustomLoginProcessingUrlCustomAuthenticationConverter {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.saml2Login((saml2) -> saml2
.loginProcessingUrl("/my/custom/url")
.authenticationConverter(AUTHENTICATION_CONVERTER)
);
// @formatter:on
return http.build();
}
}
static class Saml2LoginConfigBeans {
@Bean
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.ProviderManager;
@@ -54,6 +55,17 @@ public class AuthenticationManagerBeanDefinitionParserTests {
+ "</authentication-manager>";
// @formatter:on
// Issue #7282
// @formatter:off
private static final String CONTEXT_MULTI = "<authentication-manager id='amSecondary'>"
+ " <authentication-provider>"
+ " <user-service>"
+ " <user name='john' password='{noop}doe' authorities='ROLE_C,ROLE_D' />"
+ " </user-service>"
+ " </authentication-provider>"
+ "</authentication-manager>";
// @formatter:on
@Rule
public final SpringTestRule spring = new SpringTestRule();
@@ -64,6 +76,18 @@ public class AuthenticationManagerBeanDefinitionParserTests {
assertThat(context.getBeansOfType(AuthenticationProvider.class)).hasSize(1);
}
@Test
public void eventPublishersAreRegisteredAsTopLevelBeans() {
ConfigurableApplicationContext context = this.spring.context(CONTEXT).getContext();
assertThat(context.getBeansOfType(AuthenticationEventPublisher.class)).hasSize(1);
}
@Test
public void onlyOneEventPublisherIsRegisteredForMultipleAuthenticationManagers() {
ConfigurableApplicationContext context = this.spring.context(CONTEXT + '\n' + CONTEXT_MULTI).getContext();
assertThat(context.getBeansOfType(AuthenticationEventPublisher.class)).hasSize(1);
}
@Test
public void eventsArePublishedByDefault() throws Exception {
ConfigurableApplicationContext appContext = this.spring.context(CONTEXT).getContext();
@@ -1,5 +1,5 @@
/*
* Copyright 2009-2020 the original author or authors.
* Copyright 2009-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import org.springframework.security.util.InMemoryResource;
/**
* @author Luke Taylor
* @author Eddú Meléndez
* @author Emil Sierżęga
*/
public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext {
@@ -42,7 +43,7 @@ public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext
+ "http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd\n"
+ "http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security-";
static final String BEANS_CLOSE = "</b:beans>\n";
static final String SPRING_SECURITY_VERSION = "5.4";
static final String SPRING_SECURITY_VERSION = SpringSecurityVersions.getCurrentXsdVersionFromSpringSchemas();
Resource inMemoryXml;
@@ -0,0 +1,57 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* For computing different Spring Security versions
*/
public final class SpringSecurityVersions {
static final Pattern SCHEMA_VERSION_PATTERN = Pattern.compile("\\d+\\.\\d+(\\.\\d+)?");
public static String getCurrentXsdVersionFromSpringSchemas() {
Properties properties = new Properties();
try (InputStream is = SpringSecurityCoreVersion.class.getClassLoader()
.getResourceAsStream("META-INF/spring.schemas")) {
properties.load(is);
}
catch (IOException ex) {
throw new RuntimeException("Could not read 'META-INF/spring.schemas'", ex);
}
String inPackageLocation = properties
.getProperty("https://www.springframework.org/schema/security/spring-security.xsd");
Matcher matcher = SCHEMA_VERSION_PATTERN.matcher(inPackageLocation);
if (matcher.find()) {
return matcher.group(0);
}
throw new IllegalStateException("Failed to find version");
}
private SpringSecurityVersions() {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,10 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
/**
@@ -146,7 +149,7 @@ public class LogoutSpecTests {
}
@Test
public void logoutWhenDisabledThenPostToLogoutDoesNothing() {
public void logoutWhenDisabledThenDefaultLogoutPageDoesNotExist() {
// @formatter:off
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
@@ -156,7 +159,7 @@ public class LogoutSpecTests {
.logout().disable()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.bindToControllerAndWebFilters(HomeController.class, securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
@@ -171,8 +174,8 @@ public class LogoutSpecTests {
.submit(FormLoginTests.HomePage.class);
// @formatter:on
homePage.assertAt();
FormLoginTests.DefaultLogoutPage.to(driver).assertAt().logout();
homePage.assertAt();
FormLoginTests.DefaultLogoutPage.to(driver);
assertThat(driver.getPageSource()).isEmpty();
}
@Test
@@ -208,4 +211,14 @@ public class LogoutSpecTests {
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class).assertAt();
}
@RestController
public static class HomeController {
@GetMapping("/")
public String ok() {
return "ok";
}
}
}
+1 -15
View File
@@ -2,23 +2,15 @@ import java.util.concurrent.Callable
apply plugin: 'io.spring.convention.spring-module'
def includeProject = project(':spring-security-crypto')
configurations {
included
api.extendsFrom included
}
dependencies {
management platform(project(":spring-security-dependencies"))
api project(':spring-security-crypto')
api 'org.springframework:spring-aop'
api 'org.springframework:spring-beans'
api 'org.springframework:spring-context'
api 'org.springframework:spring-core'
api 'org.springframework:spring-expression'
included includeProject
optional 'com.fasterxml.jackson.core:jackson-databind'
optional 'io.projectreactor:reactor-core'
optional 'javax.annotation:jsr250-api'
@@ -49,12 +41,6 @@ tasks.processResources {
}
}
tasks.jar.from { includeProject.sourceSets.main.output }
project.tasks.matching { t -> t.name == "sourcesJar"}.configureEach {
from {includeProject.sourceSets.main.java }
}
configure(project.tasks.withType(Test)) {
doFirst {
systemProperties['springSecurityVersion'] = version
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2016 the original author or authors.
* Copyright 2010-2021 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.
@@ -266,7 +266,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati
* Publishes the {@link JaasAuthenticationFailedEvent}. Can be overridden by
* subclasses for different functionality
* @param token The authentication token being processed
* @param ase The excetion that caused the authentication failure
* @param ase The exception that caused the authentication failure
*/
protected void publishFailureEvent(UsernamePasswordAuthenticationToken token, AuthenticationException ase) {
if (this.applicationEventPublisher != null) {
@@ -224,7 +224,7 @@ public class JaasAuthenticationProvider extends AbstractJaasAuthenticationProvid
* Publishes the {@link JaasAuthenticationFailedEvent}. Can be overridden by
* subclasses for different functionality
* @param token The authentication token being processed
* @param ase The excetion that caused the authentication failure
* @param ase The exception that caused the authentication failure
*/
@Override
protected void publishFailureEvent(UsernamePasswordAuthenticationToken token, AuthenticationException ase) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2021 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.
@@ -129,7 +129,7 @@ public class DelegatingPasswordEncoderTests {
}
@Test
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentExcetion() {
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
+15 -15
View File
@@ -7,18 +7,18 @@ javaPlatform {
}
dependencies {
api platform("org.springframework:spring-framework-bom:5.3.7")
api platform("io.projectreactor:reactor-bom:2020.0.7")
api platform("io.rsocket:rsocket-bom:1.1.0")
api platform("org.springframework.data:spring-data-bom:2021.0.1")
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
api platform("io.projectreactor:reactor-bom:2020.0.12")
api platform("io.rsocket:rsocket-bom:1.1.1")
api platform("org.springframework.data:spring-data-bom:2021.0.6")
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.0")
api platform("com.fasterxml.jackson:jackson-bom:2.12.3")
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2")
api platform("com.fasterxml.jackson:jackson-bom:2.12.5")
constraints {
api "ch.qos.logback:logback-classic:1.2.3"
api "ch.qos.logback:logback-classic:1.2.6"
api "com.google.inject:guice:3.0"
api "com.nimbusds:nimbus-jose-jwt:9.8.1"
api "com.nimbusds:oauth2-oidc-sdk:9.3.3"
api "com.nimbusds:nimbus-jose-jwt:9.10.1"
api "com.nimbusds:oauth2-oidc-sdk:9.9.1"
api "com.squareup.okhttp3:mockwebserver:3.14.9"
api "com.squareup.okhttp3:okhttp:3.14.9"
api "com.unboundid:unboundid-ldapsdk:4.0.14"
@@ -49,10 +49,10 @@ dependencies {
api "org.assertj:assertj-core:3.19.0"
api "org.bouncycastle:bcpkix-jdk15on:1.68"
api "org.bouncycastle:bcprov-jdk15on:1.68"
api "org.eclipse.jetty:jetty-server:9.4.40.v20210413"
api "org.eclipse.jetty:jetty-servlet:9.4.40.v20210413"
api "org.eclipse.jetty:jetty-server:9.4.44.v20210927"
api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927"
api "org.eclipse.persistence:javax.persistence:2.2.1"
api "org.hibernate:hibernate-entitymanager:5.4.31.Final"
api "org.hibernate:hibernate-entitymanager:5.4.32.Final"
api "org.hsqldb:hsqldb:2.6.0"
api "org.jasig.cas.client:cas-client-core:3.6.2"
api "org.mockito:mockito-core:3.9.0"
@@ -71,9 +71,9 @@ dependencies {
api "org.seleniumhq.selenium:selenium-java:3.141.59"
api "org.seleniumhq.selenium:selenium-support:3.141.59"
api "org.skyscreamer:jsonassert:1.5.0"
api "org.slf4j:jcl-over-slf4j:1.7.30"
api "org.slf4j:log4j-over-slf4j:1.7.30"
api "org.slf4j:slf4j-api:1.7.30"
api "org.slf4j:jcl-over-slf4j:1.7.32"
api "org.slf4j:log4j-over-slf4j:1.7.32"
api "org.slf4j:slf4j-api:1.7.32"
api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE"
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
}
@@ -5,6 +5,7 @@ asciidoctor {
baseDir = file('src/docs/asciidoc')
options eruby: 'erubis'
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
attributes copycss : '',
icons : 'font',
'source-highlighter': 'prettify',
@@ -15,9 +16,10 @@ asciidoctor {
idseparator: '-',
doctype: 'book',
'spring-security-version' : project.version,
'download-url' : getDownloadUrl(),
'include-maven-repository' : getMavenRepositoryInclude(),
revnumber : project.version
revnumber : project.version,
'gh-samples-url': ghSamplesUrl,
'gh-old-samples-url': "https://github.com/spring-projects/spring-security/tree/5.4.x/samples"
}
ext.spec = copySpec {
@@ -27,11 +29,6 @@ ext.spec = copySpec {
}
}
def getDownloadUrl() {
snapshotBuild ? "https://github.com/spring-projects/spring-security/archive/main.zip" : "https://github.com/spring-projects/spring-security/archive/${project.version}.zip"
}
def getMavenRepositoryInclude() {
if(snapshotBuild) {
return "_includes/maven-repository-snapshot.asc"
@@ -12,11 +12,16 @@ download-url: the URL to download the Spring Security distribution
== Setting up the sample
This section outlines how to setup a workspace within https://spring.io/tools/sts[Spring Tool Suite (STS)] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
This section outlines how to setup a workspace within https://spring.io/tools[Eclipse IDE with Spring Tools] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
=== Obtaining the sample project
Extract the {download-url}[Spring Security Distribution] to a known location and remember it as _SPRING_SECURITY_HOME_.
Clone the {gh-samples-url}[Spring Security Samples project] to a known location and remember it as _SPRING_SECURITY_HOME_.
[NOTE]
====
These samples are being migrated to a separate project, however, you can still find
the not migrated samples in an older branch of the {gh-old-samples-url}[Spring Security repository].
====
=== Import the {starter-appname} sample application
@@ -12,11 +12,16 @@ download-url: the URL to download the Spring Security distribution
== Setting up the sample
This section outlines how to setup a workspace within https://spring.io/tools/sts[Spring Tool Suite (STS)] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
This section outlines how to setup a workspace within https://spring.io/tools[Eclipse IDE with Spring Tools] so that you can follow along with this guide. The next section outlines generic steps for how to apply Spring Security to your existing application. While you could simply apply the steps to your existing application, we encourage you to follow along with this guide in order to reduce the complexity.
=== Obtaining the sample project
Extract the {download-url}[Spring Security Distribution] to a known location and remember it as _SPRING_SECURITY_HOME_.
Clone the {gh-samples-url}[Spring Security Samples project] to a known location and remember it as _SPRING_SECURITY_HOME_.
[NOTE]
====
These samples are being migrated to a separate project, however, you can still find
the not migrated samples in an older branch of the {gh-old-samples-url}[Spring Security repository].
====
=== Import the {starter-appname} sample application
+14 -1
View File
@@ -14,13 +14,26 @@ asciidoctor {
}
asciidoctorj {
def docsTag = snapshotBuild ? 'current' : project.version
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
def ghOldSamplesUrl = "https://github.com/spring-projects/spring-security/tree/5.4.x/samples"
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
def securityApiUrl = "$securityDocsUrl/api/"
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
attributes 'spring-security-version' : project.version,
'spring-boot-version' : springBootVersion,
revnumber : project.version,
'gh-url': ghUrl,
'gh-samples-url': "$ghUrl/samples"
'gh-samples-url': ghSamplesUrl,
'gh-old-samples-url': ghOldSamplesUrl,
'security-api-url': securityApiUrl,
'security-reference-url': securityReferenceUrl,
'spring-framework-api-url': springFrameworkApiUrl,
'spring-framework-reference-url': springFrameworkReferenceUrl
attributeProvider resolvedVersions(project.configurations.testRuntimeClasspath)
}
@@ -114,7 +114,7 @@ This means that requests with the HTTP method `GET`, `HEAD`, `OPTIONS`, and `TRA
[[csrf-protection-stp]]
=== Synchronizer Token Pattern
The predominant and most comprehensive way to protect against CSRF attacks is to use the https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern[Synchronizer Token Pattern].
The predominant and most comprehensive way to protect against CSRF attacks is to use the https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern[Synchronizer Token Pattern].
This solution is to ensure that each HTTP request requires, in addition to our session cookie, a secure random generated value called a CSRF token must be present in the HTTP request.
When an HTTP request is submitted, the server must look up the expected CSRF token and compare it against the actual CSRF token in the HTTP request.
@@ -348,7 +348,7 @@ This allows the expected CSRF token to outlive the session.
One might ask why the expected CSRF token isn't stored in a cookie by default.
This is because there are known exploits in which headers (for example, to specify the cookies) can be set by another domain.
This is the same reason Ruby on Rails https://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails/[no longer skips CSRF checks when the header X-Requested-With is present].
See http://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html[this webappsec.org thread] for details on how to perform the exploit.
See https://web.archive.org/web/20210221120355/https://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html[this webappsec.org thread] for details on how to perform the exploit.
Another disadvantage is that by removing the state (that is, the timeout), you lose the ability to forcibly invalidate the token if it is compromised.
// FIXME: Document timeout with lengthy form expire. We do not want to automatically replay that request because it can lead to exploit
@@ -7,7 +7,7 @@ This portion of the documentation discusses the general topic of Security HTTP R
Refer to the relevant sections for specific information on Security HTTP Response Headers <<servlet-headers,servlet>> and <<webflux-headers,WebFlux>> based applications.
====
There are many https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Headers[HTTP response headers] that can be used to increase the security of web applications.
There are many https://owasp.org/www-project-secure-headers/#div-headers[HTTP response headers] that can be used to increase the security of web applications.
This section is dedicated to the various HTTP response headers that Spring Security provides explicit support for.
If necessary, Spring Security can also be configured to provide <<headers-custom,custom headers>>.
@@ -267,7 +267,7 @@ Content-Security-Policy: script-src https://trustedscripts.example.com; report-u
----
====
https://www.w3.org/TR/CSP2/#violation-reports[Violation reports] are standard JSON structures that can be captured either by the web application's own API or by a publicly hosted CSP violation reporting service, such as, https://report-uri.io/.
https://www.w3.org/TR/CSP2/#violation-reports[Violation reports] are standard JSON structures that can be captured either by the web application's own API or by a publicly hosted CSP violation reporting service, such as, https://report-uri.com/.
The `Content-Security-Policy-Report-Only` header provides the capability for web application authors and administrators to monitor security policies, rather than enforce them.
This header is typically used when experimenting and/or developing security policies for a site.
@@ -25,7 +25,7 @@ Without proper configuration, the application server will not know that the load
To fix this you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
To make the application aware of this, you need to either configure your application server aware of the X-Forwarded headers.
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
Alternatively, Spring users can leverage https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java[ForwardedHeaderFilter].
Spring Boot users may use the `server.use-forward-headers` property to configure the application.
@@ -24,7 +24,7 @@ The topics in this section provide detail on how to consume Spring Security when
=== Spring Boot with Maven
Spring Boot provides a `spring-boot-starter-security` starter that aggregates Spring Security-related dependencies together.
The simplest and preferred way to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/htmlsingle/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
The simplest and preferred way to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/html/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
Alternatively, you can manually add the starter, as the following example shows:
@@ -196,7 +196,7 @@ The following topics provide detail on how to consume Spring Security when using
=== Spring Boot with Gradle
Spring Boot provides a `spring-boot-starter-security` starter that aggregates Spring Security related dependencies together.
The simplest and preferred method to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/htmlsingle/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
The simplest and preferred method to use the starter is to use https://docs.spring.io/initializr/docs/current/reference/html/[Spring Initializr] by using an IDE integration (https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html[Eclipse], https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2[IntelliJ], https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour[NetBeans]) or through https://start.spring.io.
Alternatively, you can manually add the starter, as the following example shows:
@@ -2,3 +2,9 @@
= Samples
Spring Security includes many {gh-samples-url}[samples] applications.
[NOTE]
====
These samples are being migrated to a separate project, however, you can still find
the not migrated samples in an older branch of the {gh-old-samples-url}[Spring Security repository].
====
@@ -1,6 +1,6 @@
[[webflux-cors]]
== CORS
= CORS
Spring Framework provides https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-cors-intro[first class support for CORS].
CORS must be processed before Spring Security because the pre-flight request will not contain any cookies (i.e. the `JSESSIONID`).
@@ -14,6 +14,8 @@ include::webclient.adoc[leveloffset=+1]
include::method.adoc[leveloffset=+1]
include::cors.adoc[leveloffset=+1]
include::test.adoc[leveloffset=+1]
include::rsocket.adoc[leveloffset=+1]
@@ -234,4 +234,4 @@ class SecurityConfig {
----
====
You can find a complete sample in {gh-samples-url}/javaconfig/hellowebflux-method[hellowebflux-method]
You can find a complete sample in {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method]
@@ -12,7 +12,7 @@ NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as
Spring Boot 2.0 brings full auto-configuration capabilities for OAuth 2.0 Login.
This section shows how to configure the {gh-samples-url}/boot/oauth2login-webflux[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
* <<webflux-oauth2-login-sample-setup,Initial setup>>
* <<webflux-oauth2-login-sample-redirect,Setting the redirect URI>>
@@ -11,7 +11,7 @@ This authorization server can be consulted by resource servers to authorize requ
[NOTE]
====
A complete working example for {gh-samples-url}/boot/oauth2resourceserver-webflux[*JWTs*] is available in the {gh-samples-url}[Spring Security repository].
A complete working example for {gh-samples-url}/reactive/webflux/java/oauth2/resource-server[*JWTs*] is available in the {gh-samples-url}[Spring Security repository].
====
[[webflux-oauth2resourceserver-jwt-minimaldependencies]]
@@ -818,7 +818,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
====
[NOTE]
By default, Resource Server configures a clock skew of 30 seconds.
By default, Resource Server configures a clock skew of 60 seconds.
[[webflux-oauth2resourceserver-validation-custom]]
==== Configuring a Custom Validator
@@ -5,7 +5,7 @@ Spring Security allows resolving an access token using `@RegisteredOAuth2Authori
[NOTE]
====
A working example can be found in {gh-samples-url}/boot/oauth2webclient-webflux[*OAuth 2.0 WebClient WebFlux sample*].
A working example can be found in {gh-samples-url}/reactive/webflux/java/oauth2/webclient[*OAuth 2.0 WebClient WebFlux sample*].
====
After configuring Spring Security for <<webflux-oauth2-login,OAuth2 Login>> or as an <<webflux-oauth2-client,OAuth2 Client>>, an `OAuth2AuthorizedClient` can be resolved using the following:
@@ -6,7 +6,7 @@ The main entry point into security is found in the `PayloadSocketAcceptorInterce
You can find a few sample applications that demonstrate the code below:
* Hello RSocket {gh-samples-url}/boot/hellorsocket[hellorsocket]
* Hello RSocket {gh-samples-url}/reactive/rsocket/hello-security[hellorsocket]
* https://github.com/rwinch/spring-flights/tree/security[Spring Flights]
@@ -43,9 +43,8 @@ In a Spring Boot application this is done automatically using `RSocketSecurityAu
[source,java]
----
@Bean
ServerRSocketFactoryProcessor springSecurityRSocketSecurity(
SecuritySocketAcceptorInterceptor interceptor) {
return builder -> builder.addSocketAcceptorPlugin(interceptor);
RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor));
}
----
@@ -184,7 +184,7 @@ then Spring Security's test support can come in handy.
Testing the method above with `WebTestClient` would require simulating some kind of grant flow with an authorization server.
Certainly this would be a daunting task, which is why Spring Security ships with support for removing this boilerplate.
For example, we can tell Spring Security to include a default `OidcUser` using the `SecurityMockServerConfigurers#oidcLogin` method, like so:
For example, we can tell Spring Security to include a default `OidcUser` using the `SecurityMockServerConfigurers#mockOidcLogin` method, like so:
[source,java]
----
@@ -311,7 +311,7 @@ public Mono<String> foo(@AuthenticationPrincipal OAuth2User oauth2User) {
}
----
In that case, we can tell Spring Security to include a default `OAuth2User` using the `SecurityMockServerConfigurers#oauth2User` method, like so:
In that case, we can tell Spring Security to include a default `OAuth2User` using the `SecurityMockServerConfigurers#mockOAuth2Login` method, like so:
[source,java]
----
@@ -431,7 +431,7 @@ public Mono<String> foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2Author
----
Simulating this handshake with the authorization server could be cumbersome.
Instead, you can use `SecurityMockServerConfigurers#oauth2Client` to add a `OAuth2AuthorizedClient` into a mock `ServerOAuth2AuthorizedClientRepository`:
Instead, you can use `SecurityMockServerConfigurers#mockOAuth2Client` to add a `OAuth2AuthorizedClient` into a mock `ServerOAuth2AuthorizedClientRepository`:
[source,java]
----
@@ -541,7 +541,7 @@ We'll look at two of them now:
==== `mockJwt() WebTestClientConfigurer`
The first way is via a `WebTestClientConfigurer`.
The simplest of these would look something like this:
The simplest of these would be to use the `SecurityMockServerConfigurers#mockJwt` method like the following:
[source,java]
----
@@ -664,7 +664,7 @@ public Mono<String> foo(BearerTokenAuthentication authentication) {
}
----
In that case, we can tell Spring Security to include a default `BearerTokenAuthentication` using the `SecurityMockServerConfigurers#opaqueToken` method, like so:
In that case, we can tell Spring Security to include a default `BearerTokenAuthentication` using the `SecurityMockServerConfigurers#mockOpaqueToken` method, like so:
[source,java]
----
@@ -4,9 +4,9 @@
Spring Security's WebFlux support relies on a `WebFilter` and works the same for Spring WebFlux and Spring WebFlux.Fn.
You can find a few sample applications that demonstrate the code below:
* Hello WebFlux {gh-samples-url}/boot/hellowebflux[hellowebflux]
* Hello WebFlux.Fn {gh-samples-url}/boot/hellowebfluxfn[hellowebfluxfn]
* Hello WebFlux Method {gh-samples-url}/boot/hellowebflux-method[hellowebflux-method]
* Hello WebFlux {gh-samples-url}/reactive/webflux/java/hello-security[hellowebflux]
* Hello WebFlux.Fn {gh-samples-url}/reactive/webflux-fn/hello-security[hellowebfluxfn]
* Hello WebFlux Method {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method]
== Minimal WebFlux Security Configuration
@@ -257,7 +257,7 @@ To get around this, you can disable session fixation protection, but in newer Se
Note that switching between HTTP and HTTPS is not a good idea in general, as any application which uses HTTP at all is vulnerable to man-in-the-middle attacks.
To be truly secure, the user should begin accessing your site in HTTPS and continue using it until they log out.
Even clicking on an HTTPS link from a page accessed over HTTP is potentially risky.
If you need more convincing, check out a tool like https://www.thoughtcrime.org/software/sslstrip/[sslstrip].
If you need more convincing, check out a tool like https://github.com/moxie0/sslstrip/[sslstrip].
==== I'm not switching between HTTP and HTTPS but my session is still getting lost
@@ -378,7 +378,7 @@ For third-party jars the situation isn't always quite so obvious.
A good starting point is to copy those from one of the pre-built sample applications WEB-INF/lib directories.
For a basic application, you can start with the tutorial sample.
If you want to use LDAP, with an embedded test server, then use the LDAP sample as a starting point.
The reference manual also includes https://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#appendix-dependencies[an appendix] listing the first-level dependencies for each Spring Security module with some information on whether they are optional and what they are required for.
The reference manual also includes {security-reference-url}#modules[an appendix] listing the first-level dependencies for each Spring Security module with some information on whether they are optional and what they are required for.
If you are building your project with maven, then adding the appropriate Spring Security modules as dependencies to your pom.xml will automatically pull in the core jars that the framework requires.
Any which are marked as "optional" in the Spring Security POM files will have to be added to your own pom.xml file if you need them.
@@ -7,7 +7,7 @@ Without proper configuration, the application server will not know that the load
To fix this you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
To make the application aware of this, you need to either configure your application server aware of the X-Forwarded headers.
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
Alternatively, Spring 4.3+ users can leverage https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java[ForwardedHeaderFilter].
Spring Boot users may use the `server.use-forward-headers` property to configure the application.
@@ -1,7 +1,7 @@
[[servlet-delegatingfilterproxy]]
= DelegatingFilterProxy
Spring provides a `Filter` implementation named {security-api-url}org/springframework/web/filter/DelegatingFilterProxy.html/[`DelegatingFilterProxy`] that allows bridging between the Servlet container's lifecycle and Spring's `ApplicationContext`.
Spring provides a `Filter` implementation named {spring-framework-api-url}org/springframework/web/filter/DelegatingFilterProxy.html[`DelegatingFilterProxy`] that allows bridging between the Servlet container's lifecycle and Spring's `ApplicationContext`.
The Servlet container allows registering ``Filter``s using its own standards, but it is not aware of Spring defined Beans.
`DelegatingFilterProxy` can be registered via standard Servlet container mechanisms, but delegate all the work to a Spring Bean that implements `Filter`.
@@ -9,7 +9,7 @@ The picture below shows the typical layering of the handlers for a single HTTP r
image::{figures}/filterchain.png[]
The client sends a request to the application, and the container creates a `FilterChain` which contains the ``Filter``s and `Servlet` that should process the `HttpServletRequest` based on the path of the request URI.
In a Spring MVC application the `Servlet` is an instance of https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-servlet[`DispatcherServlet`].
In a Spring MVC application the `Servlet` is an instance of {spring-framework-reference-url}web.html#mvc-servlet[`DispatcherServlet`].
At most one `Servlet` can handle a single `HttpServletRequest` and `HttpServletResponse`.
However, more than one `Filter` can be used to:
@@ -100,3 +100,47 @@ This is an example of the use of the `AuthenticatedVoter` which we will see in t
It uses an `AuthenticationTrustResolver` to process this particular configuration attribute and grant access to anonymous users.
The `AuthenticatedVoter` approach is more powerful, since it allows you to differentiate between anonymous, remember-me and fully-authenticated users.
If you don't need this functionality though, then you can stick with `ROLE_ANONYMOUS`, which will be processed by Spring Security's standard `RoleVoter`.
[[anonymous-auth-mvc-controller]]
=== Getting Anonymous Authentications with Spring MVC
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments[Spring MVC resolves parameters of type `Principal`] using its own argument resolver.
This means that a construct like this one:
[source,java]
----
@GetMapping("/")
public String method(Authentication authentication) {
if (authentication instanceof AnonymousAuthenticationToken) {
return "anonymous";
} else {
return "not anonymous";
}
}
----
will always return "not anonymous", even for anonymous requests.
The reason is that Spring MVC resolves the parameter using `HttpServletRequest#getPrincipal`, which is `null` when the request is anonymous.
If you'd like to obtain the `Authentication` in anonymous requests, use `@CurrentSecurityContext` instead:
.Use CurrentSecurityContext for Anonymous requests
====
.Java
[source,java,role="primary"]
----
@GetMapping("/")
public String method(@CurrentSecurityContext SecurityContext context) {
return context.getAuthentication().getName();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@GetMapping("/")
fun method(@CurrentSecurityContext context : SecurityContext) : String =
context!!.authentication!!.name
----
====
@@ -1,7 +1,7 @@
[[servlet-events]]
== Authentication Events
For each authentication that succeeds or fails, a `AuthenticationSuccessEvent` or `AuthenticationFailureEvent` is fired, respectively.
For each authentication that succeeds or fails, a `AuthenticationSuccessEvent` or `AbstractAuthenticationFailureEvent` is fired, respectively.
To listen for these events, you must first publish an `AuthenticationEventPublisher`.
Spring Security's `DefaultAuthenticationEventPublisher` will probably do fine:
@@ -42,7 +42,7 @@ public class AuthenticationEvents {
}
@EventListener
public void onFailure(AuthenticationFailureEvent failures) {
public void onFailure(AbstractAuthenticationFailureEvent failures) {
// ...
}
}
@@ -70,7 +70,7 @@ While similar to `AuthenticationSuccessHandler` and `AuthenticationFailureHandle
=== Adding Exception Mappings
`DefaultAuthenticationEventPublisher` by default will publish an `AuthenticationFailureEvent` for the following events:
`DefaultAuthenticationEventPublisher` by default will publish an `AbstractAuthenticationFailureEvent` for the following events:
|============
| Exception | Event
@@ -97,7 +97,7 @@ To that end, you may want to supply additional mappings to the publisher via the
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
Map<Class<? extends AuthenticationException>,
Class<? extends AuthenticationFailureEvent>> mapping =
Class<? extends AbstractAuthenticationFailureEvent>> mapping =
Collections.singletonMap(FooException.class, FooEvent.class);
AuthenticationEventPublisher authenticationEventPublisher =
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
@@ -117,7 +117,7 @@ class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticatio
[[jaas-jaasauthenticationprovider]]
=== JaasAuthenticationProvider
The `JaasAuthenticationProvider` assumes the default `Configuration` is an instance of https://download.oracle.com/javase/1.4.2/docs/guide/security/jaas/spec/com/sun/security/auth/login/ConfigFile.html[ ConfigFile].
The `JaasAuthenticationProvider` assumes the default `Configuration` is an instance of https://docs.oracle.com/javase/8/docs/jre/api/security/jaas/spec/com/sun/security/auth/login/ConfigFile.html[ ConfigFile].
This assumption is made in order to attempt to update the `Configuration`.
The `JaasAuthenticationProvider` then uses the default `Configuration` to create the `LoginContext`.
@@ -147,5 +147,4 @@ It's also assumed that you have added a `UserDetailsService` (called "userDetail
The class `J2eePreAuthenticatedProcessingFilter` will extract the username from the `userPrincipal` property of the `HttpServletRequest`.
Use of this filter would usually be combined with the use of Java EE roles as described above in <<j2ee-preauth-details>>.
There is a sample application in the codebase which uses this approach, so get hold of the code from github and have a look at the application context file if you are interested.
The code is in the `samples/xml/preauth` directory.
There is a {gh-old-samples-url}/xml/preauth[sample application] in the samples project which uses this approach, so get hold of the code from GitHub and have a look at the application context file if you are interested.
@@ -51,7 +51,7 @@ If you have more than one in your application context, you need to specify which
[[remember-me-persistent-token]]
=== Persistent Token Approach
This approach is based on the article http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
There is a discussion on this in the comments section of this article.].
To use the this approach with namespace configuration, you would supply a datasource reference:
@@ -52,7 +52,7 @@ This means that you can easily use X.509 authentication with other options such
[[x509-ssl-config]]
=== Setting up SSL in Tomcat
There are some pre-generated certificates in the `samples/certificate` directory in the Spring Security project.
There are some pre-generated certificates in the {gh-samples-url}/servlet/java-configuration/authentication/x509/server[Spring Security Samples repository].
You can use these to enable SSL for testing if you don't want to generate your own.
The file `server.jks` contains the server certificate, private key and the issuing certificate authority certificate.
There are also some client certificate files for the users from the sample applications.
@@ -125,7 +125,7 @@ This should therefore work with all major databases.
At the time of writing, the system had been successfully tested using Hypersonic SQL, PostgreSQL, Microsoft SQL Server and Oracle.
Two samples ship with Spring Security that demonstrate the ACL module.
The first is the Contacts Sample, and the other is the Document Management System (DMS) Sample.
The first is the {gh-samples-url}/servlet/xml/java/contacts[Contacts Sample], and the other is the {gh-samples-url}/servlet/xml/java/dms[Document Management System (DMS) Sample].
We suggest taking a look over these for examples.
@@ -258,7 +258,7 @@ Their use is enabled through the `global-method-security` namespace element:
===== Access Control using @PreAuthorize and @PostAuthorize
The most obviously useful annotation is `@PreAuthorize` which decides whether a method can actually be invoked or not.
For example (from the "Contacts" sample application)
For example (from the {gh-samples-url}/servlet/xml/java/contacts[Contacts] sample application)
====
.Java
@@ -492,7 +492,7 @@ This would look something like this:
Where `myPermissionEvaluator` is the bean which implements `PermissionEvaluator`.
Usually this will be the implementation from the ACL module which is called `AclPermissionEvaluator`.
See the "Contacts" sample application configuration for more details.
See the {gh-samples-url}/servlet/xml/java/contacts[Contacts] sample application configuration for more details.
===== Method Security Meta Annotations
@@ -375,7 +375,7 @@ For details, refer to the <<servlet-csrf-configure-custom-repository>> section.
If a token does expire, you might want to customize how it is handled by specifying a custom `AccessDeniedHandler`.
The custom `AccessDeniedHandler` can process the `InvalidCsrfTokenException` any way you like.
For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both <<nsa-access-denied-handler,xml>> and https://github.com/spring-projects/spring-security/blob/3.2.0.RC1/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAccessDeniedHandlerTests.java#L64[Java configuration].
For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both <<nsa-access-denied-handler,xml>> and {gh-url}/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java#L64[Java configuration].
// FIXME: We should add a custom AccessDeniedHandler section in the reference and update the links above
@@ -80,7 +80,7 @@ fun httpFirewall(): StrictHttpFirewall {
----
====
The `StrictHttpFirewall` provides an allowed list of valid HTTP methods that are allowed to protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering].
The `StrictHttpFirewall` provides an allowed list of valid HTTP methods that are allowed to protect against https://owasp.org/www-community/attacks/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/06-Test_HTTP_Methods[HTTP Verb Tampering].
The default valid methods are "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT".
If your application needs to modify the valid methods, you can configure a custom `StrictHttpFirewall` bean.
For example, the following will only allow HTTP "GET" and "POST" methods:
@@ -5,7 +5,7 @@ This section covers the minimum setup for how to use Spring Security with Spring
[NOTE]
====
The completed application can be found at {gh-samples-url}/boot/helloworld[samples/boot/helloworld]
The completed application can be found {gh-samples-url}/servlet/spring-boot/java/hello-security[in our samples repository].
For your convenience, you can download a minimal Spring Boot + Spring Security application by https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello%20Security&packageName=example.hello-security&dependencies=web,security[clicking here].
====
@@ -207,7 +207,7 @@ This means developers need to explicitly protect their applications from externa
==== Spring WebSocket Allowed Origin
Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins[current domain].
Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defense_in_depth_%2528computing%2529[defence in depth].
Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[defence in depth].
[[websocket-sameorigin-csrf]]
==== Adding CSRF to Stomp Headers
@@ -20,7 +20,7 @@ This is much simpler than wiring up the equivalent Apache Directory Server beans
The most common alternative configuration requirements are supported by attributes on the `ldap-server` element and the user is isolated from worrying about which beans they need to create and what the bean property names are.
footnote:[You can find out more about the use of the `ldap-server` element in the chapter on pass:specialcharacters,macros[<<servlet-authentication-ldap>>].].
Use of a good XML editor while editing the application context file should provide information on the attributes and elements that are available.
We would recommend that you try out the https://spring.io/tools/sts[Spring Tool Suite] as it has special features for working with standard Spring namespaces.
We would recommend that you try out the https://spring.io/tools[Eclipse IDE with Spring Tools] as it has special features for working with standard Spring namespaces.
To start using the security namespace in your application context, you need to have the `spring-security-config` jar on your classpath.
@@ -12,7 +12,7 @@ NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as
Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login.
This section shows how to configure the {gh-samples-url}/boot/oauth2login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics:
* <<oauth2login-sample-initial-setup,Initial setup>>
* <<oauth2login-sample-redirect-uri,Setting the redirect URI>>
@@ -15,7 +15,7 @@ This section provides details on how Spring Security provides support for OAuth
[NOTE]
====
Working samples for both {gh-samples-url}/boot/oauth2resourceserver[JWTs] and {gh-samples-url}/boot/oauth2resourceserver-opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security repository].
Working samples for both {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/jwe[JWTs] and {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security Samples repository].
====
Let's take a look at how Bearer Token Authentication works within Spring Security.
@@ -31,7 +31,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to
image:{icondir}/number_2.png[] Spring Security's <<servlet-authorization-filtersecurityinterceptor,`FilterSecurityInterceptor`>> indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
image:{icondir}/number_3.png[] Since the user is not authenticated, <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>> initiates __Start Authentication__.
The configured <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header.
The configured <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header.
The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested.
When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token.
@@ -1068,7 +1068,7 @@ fun jwtDecoder(): JwtDecoder {
====
[NOTE]
By default, Resource Server configures a clock skew of 30 seconds.
By default, Resource Server configures a clock skew of 60 seconds.
[[oauth2resourceserver-jwt-validation-custom]]
==== Configuring a Custom Validator
@@ -2537,7 +2537,7 @@ The issuer should be one that the code can verify from a trusted source like a l
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> later on in the request.
This extra parsing can be alleviated by configuring the <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus:
This extra parsing can be alleviated by configuring the <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> directly with a `JWTClaimsSetAwareJWSKeySelector` from Nimbus:
====
.Java
@@ -2545,7 +2545,7 @@ This extra parsing can be alleviated by configuring the <<oauth2resourceserver-j
----
@Component
public class TenantJWSKeySelector
implements JWTClaimSetAwareJWSKeySelector<SecurityContext> {
implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
private final TenantRepository tenants; <1>
private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); <2>
@@ -2586,7 +2586,7 @@ public class TenantJWSKeySelector
[source,kotlin,role="secondary"]
----
@Component
class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimSetAwareJWSKeySelector<SecurityContext> {
class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
private val tenants: TenantRepository <1>
private val selectors: MutableMap<String, JWSKeySelector<SecurityContext>> = ConcurrentHashMap() <2>
@@ -3028,7 +3028,7 @@ Additionally, it is published as an `AuthenticationFailureBadCredentialsEvent`,
@Component
public class FailureEvents {
@EventListener
public void onFailure(AuthenticationFailureEvent failure) {
public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) {
if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) {
// ... handle
}
@@ -16,7 +16,7 @@ This process is similar to the one started in 2017 for <<oauth2,Spring Security'
[NOTE]
====
A working sample for {gh-samples-url}/boot/saml2login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security repository].
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
====
Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security.
-1
View File
@@ -1,7 +1,6 @@
= Spring Security Reference
Ben Alex; Luke Taylor; Rob Winch; Gunnar Hillert; Joe Grandja; Jay Bryant; Eddú Meléndez; Josh Cummings; Dave Syer; Eleftheria Stein
:include-dir: _includes
:security-api-url: https://docs.spring.io/spring-security/site/docs/current/api/
:source-indent: 0
:tabsize: 4
:toc: left
+6 -3
View File
@@ -1,9 +1,12 @@
aspectjVersion=1.9.6
aspectjVersion=1.9.7
springJavaformatVersion=0.0.28
springBootVersion=2.4.2
springFrameworkVersion=5.3.11
openSamlVersion=3.4.6
version=5.5.1-SNAPSHOT
kotlinVersion=1.5.0
version=5.5.3
kotlinVersion=1.5.31
samplesBranch=5.5.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
kotlin.stdlib.default.dependency=false
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -54,7 +54,17 @@ public class DefaultSpringSecurityContextSourceTests {
@Test
public void supportsSpacesInUrl() {
new DefaultSpringSecurityContextSource("ldap://myhost:10389/dc=spring%20framework,dc=org");
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
"ldap://myhost:10389/dc=spring%20framework,dc=org");
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("dc=spring framework,dc=org");
}
// gh-9742
@Test
public void constructorWhenUrlEncodedSpacesWithPlusCharacterThenBaseDnIsProperlyDecoded() {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
"ldap://blah:123/dc=spring+framework,dc=org ldap://blah:456/dc=spring+framework,dc=org");
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("dc=spring framework,dc=org");
}
@Test
@@ -94,6 +104,7 @@ public class DefaultSpringSecurityContextSourceTests {
public void serverUrlWithSpacesIsSupported() {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
this.contextSource.getUrls()[0] + "ou=space%20cadets,dc=springframework,dc=org");
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("ou=space cadets,dc=springframework,dc=org");
contextSource.afterPropertiesSet();
contextSource.getContext("uid=space cadet,ou=space cadets,dc=springframework,dc=org", "spacecadetspassword");
}
@@ -135,6 +146,18 @@ public class DefaultSpringSecurityContextSourceTests {
assertThat(ctxSrc.isPooled()).isTrue();
}
// gh-9742
@Test
public void constructorWhenServerListWithSpacesInBaseDnThenSuccess() {
List<String> serverUrls = new ArrayList<>();
serverUrls.add("ldap://ad1.example.org:789");
serverUrls.add("ldap://ad2.example.org:389");
serverUrls.add("ldaps://ad3.example.org:636");
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(serverUrls,
"dc=spring framework,dc=org");
assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo("dc=spring framework,dc=org");
}
@Test
public void instantiationFailsWithIncorrectServerUrl() {
List<String> serverUrls = new ArrayList<>();
@@ -0,0 +1,89 @@
/*
* Copyright 2002-2021 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.ldap.search;
import javax.naming.ldap.LdapName;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.server.ApacheDSContainer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Additional tests for {@link FilterBasedLdapUserSearch} with spaces in the base dn.
*
* @author Steve Riesenberg
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = FilterBasedLdapUserSearchWithSpacesTests.ApacheDsContainerWithSpacesConfig.class)
public class FilterBasedLdapUserSearchWithSpacesTests {
@Autowired
private DefaultSpringSecurityContextSource contextSource;
// gh-9742
@Test
public void searchForUserWhenSpacesInBaseDnThenSuccess() throws Exception {
FilterBasedLdapUserSearch locator = new FilterBasedLdapUserSearch("ou=space cadets", "(uid={0})",
this.contextSource);
locator.setSearchSubtree(false);
locator.setSearchTimeLimit(0);
locator.setDerefLinkFlag(false);
DirContextOperations bob = locator.searchForUser("space cadet");
assertThat(bob.getStringAttribute("uid")).isEqualTo("space cadet");
assertThat(bob.getDn()).isEqualTo(new LdapName("uid=space cadet,ou=space cadets"));
}
@Configuration
static class ApacheDsContainerWithSpacesConfig implements DisposableBean {
private ApacheDSContainer container;
@Bean
ApacheDSContainer ldapContainer() throws Exception {
this.container = new ApacheDSContainer("dc=spring framework,dc=org",
"classpath:test-server-with-spaces.ldif");
this.container.setPort(0);
return this.container;
}
@Bean
ContextSource contextSource(ApacheDSContainer ldapContainer) {
return new DefaultSpringSecurityContextSource(
"ldap://127.0.0.1:" + ldapContainer.getLocalPort() + "/dc=spring%20framework,dc=org");
}
@Override
public void destroy() {
this.container.stop();
}
}
}
@@ -0,0 +1,14 @@
dn: ou=space cadets,dc=spring framework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: space cadets
dn: uid=space cadet,ou=space cadets,dc=spring framework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Space Cadet
sn: Cadet
uid: space cadet
userPassword: spacecadetspassword
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,10 @@
package org.springframework.security.ldap;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
@@ -74,7 +78,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
rootDn = (rootDn != null) ? rootDn : urlRootDn;
}
setUrls(urls.toArray(new String[0]));
setBase(rootDn);
setBase((rootDn != null) ? decodeUrl(rootDn) : null);
setPooled(true);
setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy() {
@@ -136,7 +140,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
private static String buildProviderUrl(List<String> urls, String baseDn) {
Assert.notNull(baseDn, "The Base DN for the LDAP server must not be null.");
Assert.notEmpty(urls, "At least one LDAP server URL must be provided.");
String trimmedBaseDn = baseDn.trim();
String encodedBaseDn = encodeUrl(baseDn.trim());
StringBuilder providerUrl = new StringBuilder();
for (String serverUrl : urls) {
String trimmedUrl = serverUrl.trim();
@@ -147,11 +151,29 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
if (!trimmedUrl.endsWith("/")) {
providerUrl.append("/");
}
providerUrl.append(trimmedBaseDn);
providerUrl.append(encodedBaseDn);
providerUrl.append(" ");
}
return providerUrl.toString();
}
private static String encodeUrl(String url) {
try {
return URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
private String decodeUrl(String url) {
try {
return URLDecoder.decode(url, StandardCharsets.UTF_8.toString());
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
@@ -25,7 +25,7 @@ dependencies {
testImplementation 'io.projectreactor.tools:blockhound'
testImplementation 'org.skyscreamer:jsonassert'
testImplementation 'io.r2dbc:r2dbc-h2:0.8.4.RELEASE'
testImplementation 'io.r2dbc:r2dbc-spi-test:0.8.5.RELEASE'
testImplementation 'io.r2dbc:r2dbc-spi-test:0.8.6.RELEASE'
testRuntimeOnly 'org.hsqldb:hsqldb'
@@ -80,7 +80,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
private final Function<ClientRegistration, JWK> jwkResolver;
private final Map<String, NimbusJwsEncoder> jwsEncoders = new ConcurrentHashMap<>();
private final Map<String, JwsEncoderHolder> jwsEncoders = new ConcurrentHashMap<>();
/**
* Constructs a {@code NimbusJwtClientAuthenticationParametersConverter} using the
@@ -140,12 +140,16 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
JoseHeader joseHeader = headersBuilder.build();
JwtClaimsSet jwtClaimsSet = claimsBuilder.build();
NimbusJwsEncoder jwsEncoder = this.jwsEncoders.computeIfAbsent(clientRegistration.getRegistrationId(),
(clientRegistrationId) -> {
JwsEncoderHolder jwsEncoderHolder = this.jwsEncoders.compute(clientRegistration.getRegistrationId(),
(clientRegistrationId, currentJwsEncoderHolder) -> {
if (currentJwsEncoderHolder != null && currentJwsEncoderHolder.getJwk().equals(jwk)) {
return currentJwsEncoderHolder;
}
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwsEncoder(jwkSource);
return new JwsEncoderHolder(new NimbusJwsEncoder(jwkSource), jwk);
});
NimbusJwsEncoder jwsEncoder = jwsEncoderHolder.getJwsEncoder();
Jwt jws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
@@ -180,4 +184,25 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
return jwsAlgorithm;
}
private static final class JwsEncoderHolder {
private final NimbusJwsEncoder jwsEncoder;
private final JWK jwk;
private JwsEncoderHolder(NimbusJwsEncoder jwsEncoder, JWK jwk) {
this.jwsEncoder = jwsEncoder;
this.jwk = jwk;
}
private NimbusJwsEncoder getJwsEncoder() {
return this.jwsEncoder;
}
private JWK getJwk() {
return this.jwk;
}
}
}
@@ -52,7 +52,7 @@ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (!HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
if (HttpStatus.BAD_REQUEST.value() != response.getRawStatusCode()) {
this.defaultErrorHandler.handleError(response);
}
// A Bearer Token Error may be in the WWW-Authenticate response header
@@ -67,7 +67,7 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
URI endSessionEndpoint = this.endSessionEndpoint(clientRegistration);
if (endSessionEndpoint != null) {
String idToken = idToken(authentication);
URI postLogoutRedirectUri = postLogoutRedirectUri(request);
String postLogoutRedirectUri = postLogoutRedirectUri(request);
targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri);
}
}
@@ -89,7 +89,7 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
}
private URI postLogoutRedirectUri(HttpServletRequest request) {
private String postLogoutRedirectUri(HttpServletRequest request) {
if (this.postLogoutRedirectUri == null) {
return null;
}
@@ -102,11 +102,11 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
.build();
return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri)
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
.toUri();
.toUriString();
// @formatter:on
}
private String endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) {
private String endpointUri(URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
builder.queryParam("id_token_hint", idToken);
if (postLogoutRedirectUri != null) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
@@ -240,15 +239,7 @@ public final class ClientRegistrations {
() -> "The Issuer \"" + metadataIssuer + "\" provided in the configuration metadata did "
+ "not match the requested issuer \"" + issuer + "\"");
String name = URI.create(issuer).getHost();
ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer,
metadata.getTokenEndpointAuthMethods());
List<GrantType> grantTypes = metadata.getGrantTypes();
// If null, the default includes authorization_code
if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) {
throw new IllegalArgumentException(
"Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer
+ "\" returned a configuration of " + grantTypes);
}
ClientAuthenticationMethod method = getClientAuthenticationMethod(metadata.getTokenEndpointAuthMethods());
Map<String, Object> configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject());
// @formatter:off
return ClientRegistration.withRegistrationId(name)
@@ -256,7 +247,7 @@ public final class ClientRegistrations {
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(method)
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
.authorizationUri((metadata.getAuthorizationEndpointURI() != null) ? metadata.getAuthorizationEndpointURI().toASCIIString() : null)
.providerConfigurationMetadata(configurationMetadata)
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.issuerUri(issuer)
@@ -264,7 +255,7 @@ public final class ClientRegistrations {
// @formatter:on
}
private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer,
private static ClientAuthenticationMethod getClientAuthenticationMethod(
List<com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod> metadataAuthMethods) {
if (metadataAuthMethods == null || metadataAuthMethods
.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
@@ -277,10 +268,7 @@ public final class ClientRegistrations {
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) {
return ClientAuthenticationMethod.NONE;
}
throw new IllegalArgumentException(
"Only ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST and "
+ "ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer
+ "\" returned a configuration of " + metadataAuthMethods);
return null;
}
private interface ThrowingFunction<S, T, E extends Throwable> {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 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.
@@ -221,7 +221,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
Exception ex) throws IOException {
this.logger.error(LogMessage.format("Authorization Request failed: %s", ex, ex));
this.logger.error(LogMessage.format("Authorization Request failed: %s", ex), ex);
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -34,6 +34,7 @@ import org.springframework.web.server.WebSession;
* {@link OAuth2AuthorizationRequest} in the {@code WebSession}.
*
* @author Rob Winch
* @author Steve Riesenberg
* @since 5.1
* @see AuthorizationRequestRepository
* @see OAuth2AuthorizationRequest
@@ -46,6 +47,8 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
private final String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;
private boolean allowMultipleAuthorizationRequests;
@Override
public Mono<OAuth2AuthorizationRequest> loadAuthorizationRequest(ServerWebExchange exchange) {
String state = getStateParameter(exchange);
@@ -53,7 +56,9 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
return Mono.empty();
}
// @formatter:off
return getStateToAuthorizationRequest(exchange)
return this.getSessionAttributes(exchange)
.filter((sessionAttrs) -> sessionAttrs.containsKey(this.sessionAttributeName))
.map(this::getAuthorizationRequests)
.filter((stateToAuthorizationRequest) -> stateToAuthorizationRequest.containsKey(state))
.map((stateToAuthorizationRequest) -> stateToAuthorizationRequest.get(state));
// @formatter:on
@@ -63,10 +68,20 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
public Mono<Void> saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
ServerWebExchange exchange) {
Assert.notNull(authorizationRequest, "authorizationRequest cannot be null");
Assert.notNull(exchange, "exchange cannot be null");
// @formatter:off
return saveStateToAuthorizationRequest(exchange)
.doOnNext((stateToAuthorizationRequest) -> stateToAuthorizationRequest
.put(authorizationRequest.getState(), authorizationRequest))
return getSessionAttributes(exchange)
.doOnNext((sessionAttrs) -> {
if (this.allowMultipleAuthorizationRequests) {
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(
sessionAttrs);
authorizationRequests.put(authorizationRequest.getState(), authorizationRequest);
sessionAttrs.put(this.sessionAttributeName, authorizationRequests);
}
else {
sessionAttrs.put(this.sessionAttributeName, authorizationRequest);
}
})
.then();
// @formatter:on
}
@@ -78,30 +93,21 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
return Mono.empty();
}
// @formatter:off
return exchange.getSession()
.map(WebSession::getAttributes)
.handle((sessionAttrs, sink) -> {
Map<String, OAuth2AuthorizationRequest> stateToAuthzRequest = sessionAttrsMapStateToAuthorizationRequest(
return getSessionAttributes(exchange)
.flatMap((sessionAttrs) -> {
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(
sessionAttrs);
if (stateToAuthzRequest == null) {
sink.complete();
return;
}
OAuth2AuthorizationRequest removedValue = stateToAuthzRequest.remove(state);
if (stateToAuthzRequest.isEmpty()) {
OAuth2AuthorizationRequest originalRequest = authorizationRequests.remove(state);
if (authorizationRequests.isEmpty()) {
sessionAttrs.remove(this.sessionAttributeName);
}
else if (removedValue != null) {
// gh-7327 Overwrite the existing Map to ensure the state is saved for
// distributed sessions
sessionAttrs.put(this.sessionAttributeName, stateToAuthzRequest);
}
if (removedValue == null) {
sink.complete();
else if (authorizationRequests.size() == 1) {
sessionAttrs.put(this.sessionAttributeName, authorizationRequests.values().iterator().next());
}
else {
sink.next(removedValue);
sessionAttrs.put(this.sessionAttributeName, authorizationRequests);
}
return Mono.justOrEmpty(originalRequest);
});
// @formatter:on
}
@@ -120,36 +126,41 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository
return exchange.getSession().map(WebSession::getAttributes);
}
private Mono<Map<String, OAuth2AuthorizationRequest>> getStateToAuthorizationRequest(ServerWebExchange exchange) {
Assert.notNull(exchange, "exchange cannot be null");
// @formatter:off
return getSessionAttributes(exchange)
.flatMap((sessionAttrs) -> Mono.justOrEmpty(this.sessionAttrsMapStateToAuthorizationRequest(sessionAttrs)));
// @formatter:on
private Map<String, OAuth2AuthorizationRequest> getAuthorizationRequests(Map<String, Object> sessionAttrs) {
Object sessionAttributeValue = sessionAttrs.get(this.sessionAttributeName);
if (sessionAttributeValue == null) {
return new HashMap<>();
}
else if (sessionAttributeValue instanceof OAuth2AuthorizationRequest) {
OAuth2AuthorizationRequest oauth2AuthorizationRequest = (OAuth2AuthorizationRequest) sessionAttributeValue;
Map<String, OAuth2AuthorizationRequest> authorizationRequests = new HashMap<>(1);
authorizationRequests.put(oauth2AuthorizationRequest.getState(), oauth2AuthorizationRequest);
return authorizationRequests;
}
else if (sessionAttributeValue instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, OAuth2AuthorizationRequest> authorizationRequests = (Map<String, OAuth2AuthorizationRequest>) sessionAttrs
.get(this.sessionAttributeName);
return authorizationRequests;
}
else {
throw new IllegalStateException(
"authorizationRequests is supposed to be a Map or OAuth2AuthorizationRequest but actually is a "
+ sessionAttributeValue.getClass());
}
}
private Mono<Map<String, OAuth2AuthorizationRequest>> saveStateToAuthorizationRequest(ServerWebExchange exchange) {
Assert.notNull(exchange, "exchange cannot be null");
// @formatter:off
return getSessionAttributes(exchange)
.doOnNext((sessionAttrs) -> {
Object stateToAuthzRequest = sessionAttrs.get(this.sessionAttributeName);
if (stateToAuthzRequest == null) {
stateToAuthzRequest = new HashMap<String, OAuth2AuthorizationRequest>();
}
// No matter stateToAuthzRequest was in session or not, we should always put
// it into session again
// in case of redis or hazelcast session. #6215
sessionAttrs.put(this.sessionAttributeName, stateToAuthzRequest);
})
.flatMap((sessionAttrs) -> Mono.justOrEmpty(this.sessionAttrsMapStateToAuthorizationRequest(sessionAttrs)));
// @formatter:on
}
private Map<String, OAuth2AuthorizationRequest> sessionAttrsMapStateToAuthorizationRequest(
Map<String, Object> sessionAttrs) {
return (Map<String, OAuth2AuthorizationRequest>) sessionAttrs.get(this.sessionAttributeName);
/**
* Configure if multiple {@link OAuth2AuthorizationRequest}s should be stored per
* session. Default is false (not allow multiple {@link OAuth2AuthorizationRequest}
* per session).
* @param allowMultipleAuthorizationRequests true allows more than one
* {@link OAuth2AuthorizationRequest} to be stored per session.
* @since 5.5
*/
@Deprecated
public void setAllowMultipleAuthorizationRequests(boolean allowMultipleAuthorizationRequests) {
this.allowMultipleAuthorizationRequests = allowMultipleAuthorizationRequests;
}
}
@@ -16,7 +16,12 @@
package org.springframework.security.oauth2.client.endpoint;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.UUID;
import java.util.function.Function;
import com.nimbusds.jose.jwk.JWK;
@@ -42,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -172,4 +178,54 @@ public class NimbusJwtClientAuthenticationParametersConverterTests {
assertThat(jws.getExpiresAt()).isNotNull();
}
// gh-9814
@Test
public void convertWhenClientKeyChangesThenNewKeyUsed() throws Exception {
// @formatter:off
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
.build();
// @formatter:on
RSAKey rsaJwk1 = TestJwks.DEFAULT_RSA_JWK;
given(this.jwkResolver.apply(eq(clientRegistration))).willReturn(rsaJwk1);
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
clientRegistration);
MultiValueMap<String, String> parameters = this.converter.convert(clientCredentialsGrantRequest);
String encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk1.toRSAPublicKey()).build();
jwtDecoder.decode(encodedJws);
RSAKey rsaJwk2 = generateRsaJwk();
given(this.jwkResolver.apply(eq(clientRegistration))).willReturn(rsaJwk2);
parameters = this.converter.convert(clientCredentialsGrantRequest);
encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk2.toRSAPublicKey()).build();
jwtDecoder.decode(encodedJws);
}
private static RSAKey generateRsaJwk() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
}
@@ -16,12 +16,17 @@
package org.springframework.security.oauth2.client.http;
import java.io.IOException;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.web.client.UnknownHttpStatusCodeException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -67,4 +72,48 @@ public class OAuth2ErrorResponseErrorHandlerTests {
.isThrownBy(() -> this.errorHandler.handleError(response)).withMessage("[server_error] ");
}
@Test
public void handleErrorWhenErrorResponseWithInvalidStatusCodeThenHandled() {
CustomMockClientHttpResponse response = new CustomMockClientHttpResponse(new byte[0], 596);
assertThatExceptionOfType(UnknownHttpStatusCodeException.class)
.isThrownBy(() -> this.errorHandler.handleError(response)).withMessage("596 : [no body]");
}
private static final class CustomMockClientHttpResponse extends MockHttpInputMessage implements ClientHttpResponse {
private final int statusCode;
private CustomMockClientHttpResponse(byte[] content, int statusCode) {
super(content);
this.statusCode = statusCode;
}
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.valueOf(getRawStatusCode());
}
@Override
public int getRawStatusCode() {
return this.statusCode;
}
@Override
public String getStatusText() throws IOException {
HttpStatus httpStatus = HttpStatus.resolve(this.statusCode);
return (httpStatus != null) ? httpStatus.getReasonPhrase() : "";
}
@Override
public void close() {
try {
getBody().close();
}
catch (IOException ex) {
// ignore
}
}
}
}
@@ -138,6 +138,19 @@ public class OidcClientInitiatedLogoutSuccessHandlerTests {
"https://endpoint?" + "id_token_hint=id-token&" + "post_logout_redirect_uri=https://rp.example.org");
}
// gh-9511
@Test
public void logoutWhenUsingPostLogoutRedirectUriWithQueryParametersThenBuildsItForRedirect()
throws IOException, ServletException {
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(TestOidcUsers.create(),
AuthorityUtils.NO_AUTHORITIES, this.registration.getRegistrationId());
this.handler.setPostLogoutRedirectUri("https://rp.example.org/context?forwardUrl=secured%3Fparam%3Dtrue");
this.request.setUserPrincipal(token);
this.handler.onLogoutSuccess(this.request, this.response, token);
assertThat(this.response.getRedirectedUrl()).isEqualTo("https://endpoint?id_token_hint=id-token&"
+ "post_logout_redirect_uri=https://rp.example.org/context?forwardUrl%3Dsecured%253Fparam%253Dtrue");
}
@Test
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -240,24 +240,46 @@ public class ClientRegistrationsTests {
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
}
/**
* We currently only support authorization_code, so verify we have a meaningful error
* until we add support.
*/
// gh-9828
@Test
public void issuerWhenGrantTypesSupportedInvalidThenException() {
public void issuerWhenImplicitGrantTypeThenSuccess() throws Exception {
this.response.put("grant_types_supported", Arrays.asList("implicit"));
assertThatIllegalArgumentException().isThrownBy(() -> registration(""))
.withMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \""
+ this.issuer + "\" returned a configuration of [implicit]");
ClientRegistration registration = registration("").build();
// The authorization_code grant type is still the default
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
}
// gh-9828
@Test
public void issuerWhenOAuth2GrantTypesSupportedInvalidThenException() {
this.response.put("grant_types_supported", Arrays.asList("implicit"));
assertThatIllegalArgumentException().isThrownBy(() -> registrationOAuth2("", null))
.withMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \""
+ this.issuer + "\" returned a configuration of [implicit]");
public void issuerWhenOAuth2JwtBearerGrantTypeThenSuccess() throws Exception {
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
ClientRegistration registration = registrationOAuth2("", null).build();
// The authorization_code grant type is still the default
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
}
// gh-9795
@Test
public void issuerWhenResponseAuthorizationEndpointIsNullThenSuccess() throws Exception {
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
this.response.remove("authorization_endpoint");
ClientRegistration registration = registration("").authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
.build();
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
assertThat(provider.getAuthorizationUri()).isNull();
}
// gh-9795
@Test
public void issuerWhenOAuth2ResponseAuthorizationEndpointIsNullThenSuccess() throws Exception {
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
this.response.remove("authorization_endpoint");
ClientRegistration registration = registrationOAuth2("", null)
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER).build();
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
assertThat(provider.getAuthorizationUri()).isNull();
}
@Test
@@ -276,6 +298,24 @@ public class ClientRegistrationsTests {
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
// gh-9780
@Test
public void issuerWhenClientSecretBasicAuthMethodThenMethodIsBasic() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_basic"));
ClientRegistration registration = registration("").build();
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
// gh-9780
@Test
public void issuerWhenOAuth2ClientSecretBasicAuthMethodThenMethodIsBasic() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_basic"));
ClientRegistration registration = registrationOAuth2("", null).build();
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
@Test
public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
@@ -292,6 +332,46 @@ public class ClientRegistrationsTests {
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_POST);
}
// gh-9780
@Test
public void issuerWhenClientSecretJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_jwt"));
ClientRegistration registration = registration("").build();
// The client_secret_basic auth method is still the default
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
// gh-9780
@Test
public void issuerWhenOAuth2ClientSecretJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_jwt"));
ClientRegistration registration = registrationOAuth2("", null).build();
// The client_secret_basic auth method is still the default
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
// gh-9780
@Test
public void issuerWhenPrivateKeyJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("private_key_jwt"));
ClientRegistration registration = registration("").build();
// The client_secret_basic auth method is still the default
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
// gh-9780
@Test
public void issuerWhenOAuth2PrivateKeyJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("private_key_jwt"));
ClientRegistration registration = registrationOAuth2("", null).build();
// The client_secret_basic auth method is still the default
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
@Test
public void issuerWhenTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none"));
@@ -306,32 +386,24 @@ public class ClientRegistrationsTests {
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
}
/**
* We currently only support client_secret_basic, so verify we have a meaningful error
* until we add support.
*/
// gh-9780
@Test
public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() {
public void issuerWhenTlsClientAuthMethodThenSuccess() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth"));
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> registration(""))
.withMessageContaining("Only ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST and "
+ "ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer
+ "\" returned a configuration of [tls_client_auth]");
// @formatter:on
ClientRegistration registration = registration("").build();
// The client_secret_basic auth method is still the default
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
// gh-9780
@Test
public void issuerWhenOAuth2TokenEndpointAuthMethodsInvalidThenException() {
public void issuerWhenOAuth2TlsClientAuthMethodThenSuccess() throws Exception {
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth"));
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> registrationOAuth2("", null))
.withMessageContaining("Only ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST and "
+ "ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer
+ "\" returned a configuration of [tls_client_auth]");
// @formatter:on
ClientRegistration registration = registrationOAuth2("", null).build();
// The client_secret_basic auth method is still the default
assertThat(registration.getClientAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
@Test
@@ -0,0 +1,252 @@
/*
* Copyright 2002-2021 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.oauth2.client.web.server;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.WebSessionManager;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link WebSessionOAuth2ServerAuthorizationRequestRepository} when
* {@link WebSessionOAuth2ServerAuthorizationRequestRepository#setAllowMultipleAuthorizationRequests(boolean)}
* is enabled.
*
* @author Steve Riesenberg
*/
public class WebSessionOAuth2ServerAuthorizationRequestRepositoryAllowMultipleAuthorizationRequestsTests
extends WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
@Before
public void setup() {
this.repository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
this.repository.setAllowMultipleAuthorizationRequests(true);
}
@Test
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndLoad = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.loadAuthorizationRequest(oldExchange));
StepVerifier.create(saveAndSaveAndLoad)
.expectNext(oldAuthorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.expectNext(this.authorizationRequest)
.verifyComplete();
// @formatter:on
}
// gh-5145
@Test
public void loadAuthorizationRequestWhenSavedWithAllowMultipleAuthorizationRequestsThenReturnOldAuthorizationRequest() {
// save 2 requests with legacy (allowMultipleAuthorizationRequests=true) and load
// with new
WebSessionOAuth2ServerAuthorizationRequestRepository legacy = new WebSessionOAuth2ServerAuthorizationRequestRepository();
legacy.setAllowMultipleAuthorizationRequests(true);
// @formatter:off
String state1 = "state-1122";
OAuth2AuthorizationRequest authorizationRequest1 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state1)
.build();
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest1, this.exchange))
.verifyComplete();
String state2 = "state-3344";
OAuth2AuthorizationRequest authorizationRequest2 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state2)
.build();
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest2, this.exchange))
.verifyComplete();
ServerHttpRequest newRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, state1)
.build();
ServerWebExchange newExchange = this.exchange.mutate()
.request(newRequest)
.build();
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange))
.expectNext(authorizationRequest1)
.verifyComplete();
// @formatter:on
}
// gh-5145
@Test
public void saveAuthorizationRequestWhenSavedWithAllowMultipleAuthorizationRequestsThenLoadNewAuthorizationRequest() {
// save 2 requests with legacy (allowMultipleAuthorizationRequests=true), save
// with new, and load with new
WebSessionOAuth2ServerAuthorizationRequestRepository legacy = new WebSessionOAuth2ServerAuthorizationRequestRepository();
legacy.setAllowMultipleAuthorizationRequests(true);
// @formatter:off
String state1 = "state-1122";
OAuth2AuthorizationRequest authorizationRequest1 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state1)
.build();
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest1, this.exchange))
.verifyComplete();
String state2 = "state-3344";
OAuth2AuthorizationRequest authorizationRequest2 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state2)
.build();
StepVerifier.create(legacy.saveAuthorizationRequest(authorizationRequest2, this.exchange))
.verifyComplete();
String state3 = "state-5566";
OAuth2AuthorizationRequest authorizationRequest3 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state3)
.build();
ServerHttpRequest newRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, state3)
.build();
ServerWebExchange newExchange = this.exchange.mutate()
.request(newRequest)
.build();
Mono<OAuth2AuthorizationRequest> saveAndLoad = this.repository
.saveAuthorizationRequest(authorizationRequest3, this.exchange)
.then(this.repository.loadAuthorizationRequest(newExchange));
StepVerifier.create(saveAndLoad)
.expectNext(authorizationRequest3)
.verifyComplete();
// @formatter:on
}
@Test
public void removeAuthorizationRequestWhenMultipleThenOnlyOneRemoved() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.removeAuthorizationRequest(this.exchange));
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(oldExchange))
.expectNext(oldAuthorizationRequest)
.verifyComplete();
// @formatter:on
}
// gh-7327
@Test
public void removeAuthorizationRequestWhenMultipleThenRemovedAndSessionAttributeUpdated() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
Map<String, Object> sessionAttrs = spy(new HashMap<>());
WebSession session = mock(WebSession.class);
given(session.getAttributes()).willReturn(sessionAttrs);
WebSessionManager sessionManager = (e) -> Mono.just(session);
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.removeAuthorizationRequest(this.exchange));
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.verifyComplete();
// @formatter:on
verify(sessionAttrs, times(3)).put(any(), any());
}
}
@@ -0,0 +1,159 @@
/*
* Copyright 2002-2021 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.oauth2.client.web.server;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.WebSessionManager;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link WebSessionOAuth2ServerAuthorizationRequestRepository} when
* {@link WebSessionOAuth2ServerAuthorizationRequestRepository#setAllowMultipleAuthorizationRequests(boolean)}
* is disabled.
*
* @author Steve Riesenberg
*/
public class WebSessionOAuth2ServerAuthorizationRequestRepositoryDoNotAllowMultipleAuthorizationRequestsTests
extends WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
@Before
public void setup() {
this.repository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
this.repository.setAllowMultipleAuthorizationRequests(false);
}
// gh-5145
@Test
public void loadAuthorizationRequestWhenMultipleSavedThenReturnLastAuthorizationRequest() {
// @formatter:off
String state1 = "state-1122";
OAuth2AuthorizationRequest authorizationRequest1 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state1)
.build();
StepVerifier.create(this.repository.saveAuthorizationRequest(authorizationRequest1, this.exchange))
.verifyComplete();
String state2 = "state-3344";
OAuth2AuthorizationRequest authorizationRequest2 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state2)
.build();
StepVerifier.create(this.repository.saveAuthorizationRequest(authorizationRequest2, this.exchange))
.verifyComplete();
String state3 = "state-5566";
OAuth2AuthorizationRequest authorizationRequest3 = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(state3)
.build();
StepVerifier.create(this.repository.saveAuthorizationRequest(authorizationRequest3, this.exchange))
.verifyComplete();
ServerHttpRequest newRequest1 = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, state1)
.build();
ServerWebExchange newExchange1 = this.exchange.mutate()
.request(newRequest1)
.build();
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange1))
.verifyComplete();
ServerHttpRequest newRequest2 = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, state2)
.build();
ServerWebExchange newExchange2 = this.exchange.mutate()
.request(newRequest2)
.build();
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange2))
.verifyComplete();
ServerHttpRequest newRequest3 = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, state3)
.build();
ServerWebExchange newExchange3 = this.exchange.mutate()
.request(newRequest3)
.build();
StepVerifier.create(this.repository.loadAuthorizationRequest(newExchange3))
.expectNext(authorizationRequest3)
.verifyComplete();
// @formatter:on
}
// gh-5145
@Test
public void removeAuthorizationRequestWhenMultipleThenSessionAttributeRemoved() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
Map<String, Object> sessionAttrs = spy(new HashMap<>());
WebSession session = mock(WebSession.class);
given(session.getAttributes()).willReturn(sessionAttrs);
WebSessionManager sessionManager = (e) -> Mono.just(session);
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.removeAuthorizationRequest(this.exchange));
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.verifyComplete();
// @formatter:on
verify(sessionAttrs, times(2)).put(anyString(), any());
verify(sessionAttrs).remove(anyString());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,43 +16,31 @@
package org.springframework.security.oauth2.client.web.server;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.WebSessionManager;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Rob Winch
* @since 5.1
*/
public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
public abstract class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
private WebSessionOAuth2ServerAuthorizationRequestRepository repository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
protected WebSessionOAuth2ServerAuthorizationRequestRepository repository;
// @formatter:off
private OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
protected OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
@@ -60,7 +48,7 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
.build();
// @formatter:on
private ServerWebExchange exchange = MockServerWebExchange
protected ServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/").queryParam(OAuth2ParameterNames.STATE, "state"));
@Test
@@ -114,39 +102,6 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
// @formatter:on
}
@Test
public void loadAuthorizationRequestWhenMultipleSavedThenAuthorizationRequest() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndLoad = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.loadAuthorizationRequest(oldExchange));
StepVerifier.create(saveAndSaveAndLoad)
.expectNext(oldAuthorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.expectNext(this.authorizationRequest)
.verifyComplete();
// @formatter:on
}
@Test
public void saveAuthorizationRequestWhenAuthorizationRequestNullThenThrowsIllegalArgumentException() {
this.authorizationRequest = null;
@@ -211,76 +166,6 @@ public class WebSessionOAuth2ServerAuthorizationRequestRepositoryTests {
// @formatter:on
}
@Test
public void removeAuthorizationRequestWhenMultipleThenOnlyOneRemoved() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
WebSessionManager sessionManager = (e) -> this.exchange.getSession();
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.removeAuthorizationRequest(this.exchange));
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(oldExchange))
.expectNext(oldAuthorizationRequest)
.verifyComplete();
// @formatter:on
}
// gh-7327
@Test
public void removeAuthorizationRequestWhenMultipleThenRemovedAndSessionAttributeUpdated() {
String oldState = "state0";
// @formatter:off
MockServerHttpRequest oldRequest = MockServerHttpRequest.get("/")
.queryParam(OAuth2ParameterNames.STATE, oldState)
.build();
OAuth2AuthorizationRequest oldAuthorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/oauth2/authorize")
.clientId("client-id")
.redirectUri("http://localhost/client-1")
.state(oldState)
.build();
// @formatter:on
Map<String, Object> sessionAttrs = spy(new HashMap<>());
WebSession session = mock(WebSession.class);
given(session.getAttributes()).willReturn(sessionAttrs);
WebSessionManager sessionManager = (e) -> Mono.just(session);
this.exchange = new DefaultServerWebExchange(this.exchange.getRequest(), new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
ServerWebExchange oldExchange = new DefaultServerWebExchange(oldRequest, new MockServerHttpResponse(),
sessionManager, ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver());
// @formatter:off
Mono<OAuth2AuthorizationRequest> saveAndSaveAndRemove = this.repository
.saveAuthorizationRequest(oldAuthorizationRequest, oldExchange)
.then(this.repository.saveAuthorizationRequest(this.authorizationRequest, this.exchange))
.then(this.repository.removeAuthorizationRequest(this.exchange));
StepVerifier.create(saveAndSaveAndRemove).expectNext(this.authorizationRequest)
.verifyComplete();
StepVerifier.create(this.repository.loadAuthorizationRequest(this.exchange))
.verifyComplete();
// @formatter:on
verify(sessionAttrs, times(3)).put(any(), any());
}
private void assertSessionStartedIs(boolean expected) {
// @formatter:off
Mono<Boolean> isStarted = this.exchange.getSession()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -133,11 +133,13 @@ public interface ClaimAccessor {
}
/**
* Returns the claim value as a {@code Map<String, Object>} or {@code null} if it does
* not exist or cannot be assigned to a {@code Map}.
* Returns the claim value as a {@code Map<String, Object>} or {@code null} if the
* claim does not exist.
* @param claim the name of the claim
* @return the claim value or {@code null} if it does not exist or cannot be assigned
* to a {@code Map}
* @return the claim value or {@code null} if the claim does not exist
* @throws IllegalArgumentException if the claim value cannot be converted to a
* {@code List}
* @throws NullPointerException if the claim value is {@code null}
*/
@SuppressWarnings("unchecked")
default Map<String, Object> getClaimAsMap(String claim) {
@@ -156,11 +158,13 @@ public interface ClaimAccessor {
}
/**
* Returns the claim value as a {@code List<String>} or {@code null} if it does not
* exist or cannot be assigned to a {@code List}.
* Returns the claim value as a {@code List<String>} or {@code null} if the claim does
* not exist.
* @param claim the name of the claim
* @return the claim value or {@code null} if it does not exist or cannot be assigned
* to a {@code List}
* @return the claim value or {@code null} if the claim does not exist
* @throws IllegalArgumentException if the claim value cannot be converted to a
* {@code List}
* @throws NullPointerException if the claim value is {@code null}
*/
@SuppressWarnings("unchecked")
default List<String> getClaimAsStringList(String claim) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -90,7 +90,7 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
private OAuth2Error createOAuth2Error(String reason) {
this.logger.debug(reason);
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, reason,
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason,
"https://tools.ietf.org/html/rfc6750#section-3.1");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -52,18 +52,19 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
private final Map<String, Converter<Object, ?>> claimTypeConverters;
private final Converter<Map<String, Object>, Map<String, Object>> delegate;
/**
* Constructs a {@link MappedJwtClaimSetConverter} with the provided arguments
*
* This will completely replace any set of default converters.
*
* A converter that returns {@code null} removes the claim from the claim set. A
* converter that returns a non-{@code null} value adds or replaces that claim in the
* claim set.
* @param claimTypeConverters The {@link Map} of converters to use
*/
public MappedJwtClaimSetConverter(Map<String, Converter<Object, ?>> claimTypeConverters) {
Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null");
this.claimTypeConverters = claimTypeConverters;
this.delegate = new ClaimTypeConverter(claimTypeConverters);
}
/**
@@ -87,6 +88,10 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
*
* To completely replace the underlying {@link Map} of converters, see
* {@link MappedJwtClaimSetConverter#MappedJwtClaimSetConverter(Map)}.
*
* A converter that returns {@code null} removes the claim from the claim set. A
* converter that returns a non-{@code null} value adds or replaces that claim in the
* claim set.
* @param claimTypeConverters
* @return An instance of {@link MappedJwtClaimSetConverter} that contains the
* converters provided, plus any defaults that were not overridden.
@@ -143,9 +148,16 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
@Override
public Map<String, Object> convert(Map<String, Object> claims) {
Assert.notNull(claims, "claims cannot be null");
Map<String, Object> mappedClaims = this.delegate.convert(claims);
mappedClaims = removeClaims(mappedClaims);
mappedClaims = addClaims(mappedClaims);
Map<String, Object> mappedClaims = new HashMap<>(claims);
for (Map.Entry<String, Converter<Object, ?>> entry : this.claimTypeConverters.entrySet()) {
String claimName = entry.getKey();
Converter<Object, ?> converter = entry.getValue();
if (converter != null) {
Object claim = claims.get(claimName);
Object mappedClaim = converter.convert(claim);
mappedClaims.compute(claimName, (key, value) -> mappedClaim);
}
}
Instant issuedAt = (Instant) mappedClaims.get(JwtClaimNames.IAT);
Instant expiresAt = (Instant) mappedClaims.get(JwtClaimNames.EXP);
if (issuedAt == null && expiresAt != null) {
@@ -154,24 +166,4 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
return mappedClaims;
}
private Map<String, Object> removeClaims(Map<String, Object> claims) {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Object> entry : claims.entrySet()) {
if (entry.getValue() != null) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
private Map<String, Object> addClaims(Map<String, Object> claims) {
Map<String, Object> result = new HashMap<>(claims);
for (Map.Entry<String, Converter<Object, ?>> entry : this.claimTypeConverters.entrySet()) {
if (!claims.containsKey(entry.getKey()) && entry.getValue().convert(null) != null) {
result.put(entry.getKey(), entry.getValue().convert(null));
}
}
return result;
}
}
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
@@ -109,6 +110,7 @@ public class JwtTimestampValidatorTests {
.collect(Collectors.toList());
// @formatter:on
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(messages).contains("Jwt used before " + justOverOneDayFromNow);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 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.
@@ -123,8 +123,18 @@ public class MappedJwtClaimSetConverterTests {
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234");
}
// gh-10135
@Test
public void convertWhenConverterReturnsNullThenClaimIsRemoved() {
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap(JwtClaimNames.NBF, (nbfClaimValue) -> null));
Map<String, Object> source = Collections.singletonMap(JwtClaimNames.NBF, Instant.now());
Map<String, Object> target = converter.convert(source);
assertThat(target).doesNotContainKey(JwtClaimNames.NBF);
}
@Test
public void convertWhenClaimValueIsNullThenClaimIsRemoved() {
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
Map<String, Object> source = Collections.singletonMap(JwtClaimNames.ISS, null);
Map<String, Object> target = converter.convert(source);
@@ -1,5 +1,5 @@
/*
* Copyright 2019 the original author or authors.
* Copyright 2019-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -91,14 +91,17 @@ class PayloadInterceptorRSocket extends RSocketProxy {
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
return Flux.from(payloads).switchOnFirst((signal, innerFlux) -> {
Payload firstPayload = signal.get();
return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload).flatMapMany((context) -> innerFlux
.skip(1).flatMap((p) -> intercept(PayloadExchangeType.PAYLOAD, p).thenReturn(p))
.transform((securedPayloads) -> Flux.concat(Flux.just(firstPayload), securedPayloads))
.transform((securedPayloads) -> this.source.requestChannel(securedPayloads))
.subscriberContext(context));
return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload).flatMapMany(
(context) -> innerFlux.index().concatMap((tuple) -> justOrIntercept(tuple.getT1(), tuple.getT2()))
.transform((securedPayloads) -> this.source.requestChannel(securedPayloads))
.subscriberContext(context));
});
}
private Mono<Payload> justOrIntercept(Long index, Payload payload) {
return (index == 0) ? Mono.just(payload) : intercept(PayloadExchangeType.PAYLOAD, payload).thenReturn(payload);
}
@Override
public Mono<Void> metadataPush(Payload payload) {
return intercept(PayloadExchangeType.METADATA_PUSH, payload)
@@ -1,5 +1,5 @@
/*
* Copyright 2019 the original author or authors.
* Copyright 2019-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,10 +19,14 @@ package org.springframework.security.rsocket.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.metadata.WellKnownMimeType;
import io.rsocket.util.ByteBufPayload;
import io.rsocket.util.DefaultPayload;
import io.rsocket.util.RSocketProxy;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,13 +36,17 @@ import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import reactor.test.publisher.TestPublisher;
import reactor.util.context.Context;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@@ -56,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -265,6 +274,57 @@ public class PayloadInterceptorRSocketTests {
verify(this.delegate).requestChannel(any());
}
// gh-9345
@Test
public void requestChannelWhenInterceptorCompletesThenAllPayloadsRetained() {
ExecutorService executors = Executors.newSingleThreadExecutor();
Payload payload = ByteBufPayload.create("data");
Payload payloadTwo = ByteBufPayload.create("moredata");
Payload payloadThree = ByteBufPayload.create("stillmoredata");
Context ctx = Context.empty();
Flux<Payload> payloads = this.payloadResult.flux();
given(this.interceptor.intercept(any(), any())).willReturn(Mono.empty())
.willReturn(Mono.error(() -> new AccessDeniedException("Access Denied")));
given(this.delegate.requestChannel(any())).willAnswer((invocation) -> {
Flux<Payload> input = invocation.getArgument(0);
return Flux.from(input).switchOnFirst((signal, innerFlux) -> innerFlux.map(Payload::getDataUtf8)
.transform((data) -> Flux.<String>create((emitter) -> {
Runnable run = () -> data.subscribe(new CoreSubscriber<String>() {
@Override
public void onSubscribe(Subscription s) {
s.request(3);
}
@Override
public void onNext(String s) {
emitter.next(s);
}
@Override
public void onError(Throwable t) {
emitter.error(t);
}
@Override
public void onComplete() {
emitter.complete();
}
});
executors.execute(run);
})).map(DefaultPayload::create));
});
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), this.metadataMimeType, this.dataMimeType, ctx);
StepVerifier.create(interceptor.requestChannel(payloads).doOnDiscard(Payload.class, Payload::release))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(payload, payloadTwo, payloadThree))
.assertNext((next) -> assertThat(next.getDataUtf8()).isEqualTo(payload.getDataUtf8()))
.verifyError(AccessDeniedException.class);
verify(this.interceptor, times(2)).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(payloadTwo);
verify(this.delegate).requestChannel(any());
}
@Test
public void requestChannelWhenInterceptorErrorsThenDelegateNotSubscribed() {
RuntimeException expected = new RuntimeException("Oops");

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