Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57c2af4900 | |||
| 6da4ea0d0d | |||
| 8f69721f24 | |||
| acd131c3d1 | |||
| 73b077790f | |||
| 62c4d60524 | |||
| d884913175 | |||
| fd5dae5e4d | |||
| d29c761135 | |||
| 700a4533aa | |||
| 3a394c633f | |||
| 79f9bec818 | |||
| 5b019365b7 | |||
| 038da0289e | |||
| f9b4afdfc5 | |||
| 65aaedbc83 | |||
| f325ddbdaa | |||
| bc9bdbaf83 | |||
| 6da29433cc | |||
| 4adfdf66b9 | |||
| e91b81a5aa | |||
| 0949d3b3ea | |||
| d44efc899d | |||
| c5870a746a | |||
| 58bca0e895 | |||
| 1bc3fd9ea9 | |||
| e84b949719 | |||
| 33bd1abb9d | |||
| ea62b200f7 | |||
| 5eb41b8a06 | |||
| 8f0c077fc7 | |||
| 430b9bb8c6 | |||
| 0fa81bfb6d | |||
| 54e9575391 | |||
| e3ad551ab3 | |||
| 7fc97c6ee2 | |||
| 241bf2c148 | |||
| cf06871200 | |||
| e50c2a6a74 | |||
| b175e4487e | |||
| 356131b1ea | |||
| 97a49aa3bf | |||
| e4a78c26d6 | |||
| c50e4c02bb | |||
| a14c9d66b1 | |||
| 2924dd29f7 | |||
| 9d4d9065b4 | |||
| 3f1a794cd9 | |||
| c5de4096b4 | |||
| 95d375c8d8 | |||
| b486d2695a | |||
| 8b34113c2a | |||
| fe2e52f646 | |||
| 0ea87ca3bf | |||
| 71fd23429c | |||
| 85514a9b34 | |||
| d039e0191f | |||
| cfdadfea01 | |||
| f70aa75445 | |||
| 86e0f1341e | |||
| cea6618265 | |||
| 5bf8f641f1 | |||
| 8b9f556d8f | |||
| d9b8a8ec54 | |||
| 4e04c266be | |||
| dc5c65eaf1 | |||
| 0353616be3 | |||
| 47853e1314 | |||
| 90e1658afe | |||
| e1d94d1e06 | |||
| a07c67a365 | |||
| 16cb219e4a | |||
| 944149ba71 | |||
| 9b14465243 | |||
| fdc0fd23df | |||
| 931dfbe8e7 | |||
| b075f0df02 | |||
| 8720a282ce | |||
| 53bc6a7796 | |||
| aed95aa2b1 | |||
| 6343002b32 | |||
| 7b57a4a3bb | |||
| 0a9d4dc8fc | |||
| 11047960ad | |||
| 3d4e20597a | |||
| 4c40eeb89e | |||
| ff1e925c08 | |||
| 7b309cf271 | |||
| 51a1f88ddf | |||
| 762d8f19e5 | |||
| c9d623966f | |||
| 81bd52ae48 | |||
| 25b6af2738 | |||
| 95987bffc1 | |||
| e96203d578 | |||
| 1637727750 | |||
| 6e5f8f2a1d | |||
| 3ef1c34632 | |||
| 4187af38b2 | |||
| 19b3cae62e | |||
| a2358bbc83 | |||
| fbe09c26ae | |||
| 5b638a54a4 | |||
| d0d2ac545b | |||
| 51eef2b980 | |||
| 63cd037188 | |||
| d3e349c290 | |||
| bc16a6f723 | |||
| e480da69e9 | |||
| 1d466f86a0 | |||
| 9dfa5cf713 | |||
| bd8bdb8a98 | |||
| 302cfb116e | |||
| 695ea1717f | |||
| 1206c2b141 | |||
| 3539f06146 | |||
| 4a6e0a13cd | |||
| fc630ae6eb | |||
| a317a3d866 | |||
| 88118afb8f | |||
| 9ad7981e36 | |||
| 68b820ed09 | |||
| 53bcf0d16b | |||
| 438c783c7d | |||
| 41524880c6 | |||
| 2361dc131e | |||
| 44d32815b1 | |||
| 87c3335e01 | |||
| 76e9d91f24 | |||
| 77fe9e892a | |||
| eefbb4da64 | |||
| 8f65f88dc0 | |||
| a2793f31b4 | |||
| 64d8e6cc9b | |||
| 679a47a51d | |||
| d4678c8e04 | |||
| 43b132bec6 | |||
| 08fca57d12 | |||
| acabacb971 | |||
| 1a130fca3c | |||
| 067f79dde5 | |||
| 45758a5cec | |||
| 52d98ab7af | |||
| 0b680be97b | |||
| 7c28b15471 | |||
| abf3c866fb | |||
| 5a4ada04ac | |||
| c08329c0c5 | |||
| a856baa6a8 | |||
| 611786e4b5 | |||
| ac63cf4fa5 | |||
| f6bb55effb | |||
| 6020ab8e65 | |||
| 3076367168 | |||
| 721b22d87a | |||
| 85b756cb74 | |||
| 0ce76d2c5d | |||
| 66cf02c6b0 | |||
| 7441ce7f16 | |||
| 9dbcd8cf00 | |||
| e6db4418b0 | |||
| 835d6c1fbd | |||
| 9fb3e14989 | |||
| 2c90edd7b7 | |||
| 95b2cdf7f4 | |||
| f0e71a8bc4 | |||
| 3ecf84855e | |||
| 2848b95fe0 | |||
| 0039bc0cf0 | |||
| 671a53e850 | |||
| 057e5181ea | |||
| 178ca56aaf | |||
| 164fbaf007 | |||
| 61ccf14953 | |||
| 65cf2586c5 | |||
| 6e683f2286 | |||
| bae4cdd765 | |||
| a7c3e842d6 | |||
| b6e24db68c | |||
| 7dea8b8ca2 | |||
| aeb5fc1fb0 | |||
| 4542f58be7 | |||
| 62f33d3fcf | |||
| 956561e143 | |||
| 9fed1ac8c3 | |||
| 9dbe3bdcc0 | |||
| d547ae0181 | |||
| b8b1278e1f | |||
| 381047e386 | |||
| fbbbd46bee | |||
| 8abffbd0df | |||
| 376b40a735 | |||
| 89fa1cbdd2 | |||
| 0d75e6d10c | |||
| 01758c4c59 | |||
| f37833a59c | |||
| 52e6c4c4be | |||
| 874dce4407 | |||
| f21e8af830 | |||
| 8aae3490da | |||
| 96ceb535f4 | |||
| 0c54a55ae8 |
@@ -14,7 +14,7 @@ permissions:
|
||||
jobs:
|
||||
snapshot-test:
|
||||
name: Test Against Snapshots
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -33,6 +33,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: inherit
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build]
|
||||
needs: [ build ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
|
||||
@@ -15,24 +15,24 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Compute Version
|
||||
id: compute-version
|
||||
uses: spring-io/spring-release-actions/compute-version@0.0.3
|
||||
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||
- name: Get Today's Release Version
|
||||
id: todays-release
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@0.0.3
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||
with:
|
||||
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
||||
milestone-repository: ${{ github.repository }}
|
||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute Next Version
|
||||
id: next-version
|
||||
uses: spring-io/spring-release-actions/compute-next-version@0.0.3
|
||||
uses: spring-io/spring-release-actions/compute-next-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||
with:
|
||||
version: ${{ steps.todays-release.outputs.release-version }}
|
||||
- name: Schedule Next Milestone
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@0.0.3
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||
with:
|
||||
version: ${{ steps.next-version.outputs.version }}
|
||||
version-date: ${{ steps.next-version.outputs.version-date }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -16,7 +16,7 @@ permissions:
|
||||
jobs:
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
|
||||
with:
|
||||
should-perform-release: true
|
||||
project-version: ${{ inputs.version }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
|
||||
@@ -30,6 +30,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -11,9 +11,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
@@ -46,6 +46,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Dispatch
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
matrix:
|
||||
branch: [ '6.4.x', '6.5.x', 'main' ]
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: ${{ matrix.branch }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on docs-build
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: 'docs-build'
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
jobs:
|
||||
update-scheduled-release-version:
|
||||
name: Update Scheduled Release Version
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
+1
@@ -35,6 +35,7 @@ import org.springframework.security.web.FilterInvocation;
|
||||
*/
|
||||
@Deprecated
|
||||
@NullUnmarked
|
||||
@SuppressWarnings("serial")
|
||||
class WebExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<FilterInvocation> {
|
||||
|
||||
private final Expression authorizeExpression;
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.security.jackson.SecurityJacksonModules;
|
||||
* @since 7.0
|
||||
* @see SecurityJacksonModules
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class CasJacksonModule extends SecurityJacksonModule {
|
||||
|
||||
public CasJacksonModule() {
|
||||
|
||||
+3
-1
@@ -2035,7 +2035,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
*/
|
||||
public HttpSecurity securityMatcher(String... patterns) {
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
PathPatternRequestMatcher.Builder builder = getSharedObject(PathPatternRequestMatcher.Builder.class);
|
||||
ApplicationContext context = getSharedObject(ApplicationContext.class);
|
||||
PathPatternRequestMatcher.Builder builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
|
||||
.getIfUnique(() -> getSharedObject(PathPatternRequestMatcher.Builder.class));
|
||||
for (String pattern : patterns) {
|
||||
matchers.add(builder.matcher(pattern));
|
||||
}
|
||||
|
||||
+3
@@ -39,6 +39,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
|
||||
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
|
||||
import org.springframework.security.web.webauthn.management.CredentialRecordOwnerAuthorizationManager;
|
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
@@ -180,6 +181,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter);
|
||||
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
|
||||
rpOperations);
|
||||
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
|
||||
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
|
||||
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
|
||||
rpOperations);
|
||||
if (creationOptionsRepository != null) {
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ public final class PathPatternRequestMatcherFactoryBean
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (this.basePath != null) {
|
||||
this.builder.basePath(this.basePath);
|
||||
this.builder = this.builder.basePath(this.basePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
@@ -85,6 +86,9 @@ import org.springframework.security.authentication.password.CompromisedPasswordE
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.FactorAuthorizationDecision;
|
||||
import org.springframework.security.authorization.RequiredFactor;
|
||||
import org.springframework.security.authorization.RequiredFactorError;
|
||||
import org.springframework.security.authorization.event.AuthorizationEvent;
|
||||
import org.springframework.security.authorization.event.AuthorizationGrantedEvent;
|
||||
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
|
||||
@@ -161,6 +165,7 @@ import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidationException;
|
||||
import org.springframework.security.oauth2.jwt.TestJwts;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
@@ -168,15 +173,22 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntro
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
@@ -190,6 +202,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
|
||||
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
|
||||
@@ -249,6 +262,7 @@ import org.springframework.security.web.webauthn.api.AuthenticationExtensionsCli
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
|
||||
@@ -263,6 +277,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestO
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestBytes;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
|
||||
@@ -427,6 +442,8 @@ final class SerializationSamples {
|
||||
generatorByClassName.put(RegisteredClient.class, (r) -> registeredClient);
|
||||
generatorByClassName.put(OAuth2Authorization.class, (r) -> authorization);
|
||||
generatorByClassName.put(OAuth2Authorization.Token.class, (r) -> authorization.getAccessToken());
|
||||
generatorByClassName.put(OAuth2AuthorizationCode.class,
|
||||
(r) -> new OAuth2AuthorizationCode("code", Instant.now(), Instant.now().plusSeconds(300)));
|
||||
generatorByClassName.put(OAuth2AuthorizationConsent.class,
|
||||
(r) -> OAuth2AuthorizationConsent.withId("registeredClientId", "principalName")
|
||||
.scope("scope1")
|
||||
@@ -452,6 +469,58 @@ final class SerializationSamples {
|
||||
authenticationToken.setDetails(details);
|
||||
return authenticationToken;
|
||||
});
|
||||
generatorByClassName.put(
|
||||
org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken.class,
|
||||
(r) -> {
|
||||
org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken token = new org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken(
|
||||
"code", principal, "https://localhost/callback", Map.of("custom_param", "custom_value"));
|
||||
token.setDetails(details);
|
||||
return token;
|
||||
});
|
||||
generatorByClassName.put(OAuth2AuthorizationCodeRequestAuthenticationException.class, (r) -> {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authToken = new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
"https://localhost/authorize", "clientId", principal, "https://localhost/callback", "state",
|
||||
authorizationRequest.getScopes(), authorizationRequest.getAdditionalParameters());
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationException(
|
||||
new OAuth2Error("invalid_request", "Missing required parameter", "https://example.com/error"),
|
||||
authToken);
|
||||
});
|
||||
generatorByClassName.put(OAuth2ClientCredentialsAuthenticationToken.class, (r) -> {
|
||||
OAuth2ClientCredentialsAuthenticationToken token = new OAuth2ClientCredentialsAuthenticationToken(principal,
|
||||
Set.of("scope1", "scope2"), Map.of("custom_param", "custom_value"));
|
||||
token.setDetails(details);
|
||||
return token;
|
||||
});
|
||||
generatorByClassName.put(OAuth2DeviceCodeAuthenticationToken.class, (r) -> {
|
||||
OAuth2DeviceCodeAuthenticationToken token = new OAuth2DeviceCodeAuthenticationToken("device-code",
|
||||
principal, Map.of("custom_param", "custom_value"));
|
||||
token.setDetails(details);
|
||||
return token;
|
||||
});
|
||||
generatorByClassName.put(OAuth2RefreshTokenAuthenticationToken.class, (r) -> {
|
||||
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken("refresh-token",
|
||||
principal, Set.of("scope1", "scope2"), Map.of("custom_param", "custom_value"));
|
||||
token.setDetails(details);
|
||||
return token;
|
||||
});
|
||||
generatorByClassName.put(OAuth2TokenExchangeAuthenticationToken.class, (r) -> {
|
||||
OAuth2TokenExchangeAuthenticationToken token = new OAuth2TokenExchangeAuthenticationToken(
|
||||
"urn:ietf:params:oauth:token-type:access_token", "subject-token",
|
||||
"urn:ietf:params:oauth:token-type:jwt", principal, "actor-token",
|
||||
"urn:ietf:params:oauth:token-type:jwt", Set.of("https://resource.example.com"), Set.of("audience"),
|
||||
Set.of("scope1"), Map.of("custom_param", "custom_value"));
|
||||
token.setDetails(details);
|
||||
return token;
|
||||
});
|
||||
OAuth2TokenExchangeActor actor = new OAuth2TokenExchangeActor(Map.of(OAuth2TokenClaimNames.ISS,
|
||||
"https://issuer.example.com", OAuth2TokenClaimNames.SUB, "actor-subject"));
|
||||
generatorByClassName.put(OAuth2TokenExchangeActor.class, (r) -> actor);
|
||||
generatorByClassName.put(OAuth2TokenExchangeCompositeAuthenticationToken.class, (r) -> {
|
||||
AbstractAuthenticationToken token = new OAuth2TokenExchangeCompositeAuthenticationToken(authentication,
|
||||
List.of(actor));
|
||||
token.setDetails(details);
|
||||
return token;
|
||||
});
|
||||
generatorByClassName.put(OAuth2AuthorizationConsentAuthenticationToken.class, (r) -> {
|
||||
OAuth2AuthorizationConsentAuthenticationToken authenticationToken = new OAuth2AuthorizationConsentAuthenticationToken(
|
||||
"authorizationUri", "clientId", principal, "state", authorizationRequest.getScopes(),
|
||||
@@ -668,6 +737,12 @@ final class SerializationSamples {
|
||||
generatorByClassName.put(AuthorizationDecision.class, (r) -> new AuthorizationDecision(true));
|
||||
generatorByClassName.put(AuthorityAuthorizationDecision.class,
|
||||
(r) -> new AuthorityAuthorizationDecision(true, AuthorityUtils.createAuthorityList("ROLE_USER")));
|
||||
RequiredFactor factor = RequiredFactor.withAuthority("authority").validDuration(Duration.ofSeconds(5)).build();
|
||||
generatorByClassName.put(RequiredFactor.class, (r) -> factor);
|
||||
RequiredFactorError error = RequiredFactorError.createMissing(factor);
|
||||
generatorByClassName.put(RequiredFactorError.class, (r) -> error);
|
||||
generatorByClassName.put(FactorAuthorizationDecision.class,
|
||||
(r) -> new FactorAuthorizationDecision(List.of(error)));
|
||||
generatorByClassName.put(CycleInRoleHierarchyException.class, (r) -> new CycleInRoleHierarchyException());
|
||||
generatorByClassName.put(AuthorizationEvent.class,
|
||||
(r) -> new AuthorizationEvent(new SerializableSupplier<>(authentication), "source",
|
||||
@@ -858,6 +933,8 @@ final class SerializationSamples {
|
||||
generatorByClassName.put(CredentialPropertiesOutput.class, (o) -> credentialOutput);
|
||||
generatorByClassName.put(ImmutableAuthenticationExtensionsClientOutputs.class, (o) -> outputs);
|
||||
generatorByClassName.put(AuthenticatorAssertionResponse.class, (r) -> response);
|
||||
generatorByClassName.put(AuthenticatorAttestationResponse.class,
|
||||
(r) -> TestAuthenticatorAttestationResponses.createAuthenticatorAttestationResponse().build());
|
||||
generatorByClassName.put(RelyingPartyAuthenticationRequest.class, (r) -> authRequest);
|
||||
generatorByClassName.put(PublicKeyCredential.class, (r) -> credential);
|
||||
generatorByClassName.put(WebAuthnAuthenticationRequestToken.class, (r) -> requestToken);
|
||||
|
||||
+54
-5
@@ -33,10 +33,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
@@ -207,10 +207,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
boolean hasSerialVersion = Stream.of(clazz.getDeclaredFields())
|
||||
.map(Field::getName)
|
||||
.anyMatch((n) -> n.equals("serialVersionUID"));
|
||||
SuppressWarnings suppressWarnings = clazz.getAnnotation(SuppressWarnings.class);
|
||||
boolean hasSerialIgnore = suppressWarnings == null
|
||||
|| Arrays.asList(suppressWarnings.value()).contains("Serial");
|
||||
if (!hasSerialVersion && !hasSerialIgnore) {
|
||||
if (!hasSerialVersion && !hasSuppressSerialInSource(clazz)) {
|
||||
classes.add(clazz);
|
||||
continue;
|
||||
}
|
||||
@@ -249,6 +246,58 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
return classes.stream();
|
||||
}
|
||||
|
||||
private static boolean hasSuppressSerialInSource(Class<?> clazz) {
|
||||
try {
|
||||
Class<?> fileClass = clazz;
|
||||
while (fileClass.getEnclosingClass() != null) {
|
||||
fileClass = fileClass.getEnclosingClass();
|
||||
}
|
||||
var codeSource = fileClass.getProtectionDomain().getCodeSource();
|
||||
if (codeSource == null) {
|
||||
return false;
|
||||
}
|
||||
Path sourceFile = findSourceFile(Path.of(codeSource.getLocation().toURI()), fileClass);
|
||||
if (sourceFile == null) {
|
||||
return false;
|
||||
}
|
||||
return hasSuppressSerialAnnotation(Files.readAllLines(sourceFile), clazz.getSimpleName());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path findSourceFile(Path start, Class<?> clazz) {
|
||||
String relativePath = clazz.getName().replace('.', '/') + ".java";
|
||||
Path dir = start;
|
||||
for (int i = 0; i < 10 && dir != null; i++) {
|
||||
for (String sourceRoot : List.of("src/main/java", "src/test/java")) {
|
||||
Path candidate = dir.resolve(sourceRoot).resolve(relativePath);
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
dir = dir.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean hasSuppressSerialAnnotation(List<String> lines, String simpleClassName) {
|
||||
Pattern classDeclaration = Pattern
|
||||
.compile("\\b(?:class|interface|enum|record)\\s+" + Pattern.quote(simpleClassName) + "\\b");
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (classDeclaration.matcher(lines.get(i)).find()) {
|
||||
for (int j = Math.max(0, i - 5); j < i; j++) {
|
||||
String line = lines.get(j);
|
||||
if (line.contains("@SuppressWarnings") && line.contains("\"serial\"")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getCurrentVersion() {
|
||||
String version = System.getProperty("springSecurityVersion");
|
||||
String[] parts = version.split("\\.");
|
||||
|
||||
+62
@@ -37,6 +37,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -110,6 +111,21 @@ public class WebSecurityTests {
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void ignoringWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception {
|
||||
loadConfig(IgnoringBuilderBeanConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
setup();
|
||||
this.request.setServletPath("");
|
||||
this.request.setRequestURI("/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
@@ -201,6 +217,52 @@ public class WebSecurityTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
static class IgnoringBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/path");
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().denyAll());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping("/path")
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestRejectedHandlerConfig {
|
||||
|
||||
+38
@@ -452,6 +452,18 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenBuilderBeanWithBasePathAndRawStringThenHonorsBasePath() throws Exception {
|
||||
this.spring.register(RequestMatchersRawStringServletPathConfig.class, BasicController.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder matchedByBasePath = get("/spring/path")
|
||||
.servletPath("/spring")
|
||||
.with(user("user").roles("USER"));
|
||||
// @formatter:on
|
||||
this.mvc.perform(matchedByBasePath).andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/path").with(user("user").roles("USER"))).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception {
|
||||
this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire();
|
||||
@@ -1359,6 +1371,32 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
static class RequestMatchersRawStringServletPathConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
return http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/path").hasRole("ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class AuthenticatedConfig {
|
||||
|
||||
+43
@@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.AuthenticationTestConfiguration;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextChangedListener;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
@@ -153,6 +154,17 @@ public class FormLoginConfigurerTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void loginWhenBuilderBeanWithBasePathThenLoginProcessingUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(FormLoginBuilderBeanConfig.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(formLogin().user("invalid"))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?error"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenFormLoginConfiguredThenHasDefaultSuccessUrl() throws Exception {
|
||||
this.spring.register(FormLoginConfig.class).autowire();
|
||||
@@ -519,6 +531,37 @@ public class FormLoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class FormLoginBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.formLogin(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class FormLoginInLambdaConfig {
|
||||
|
||||
+105
@@ -125,6 +125,34 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityMatcherWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception {
|
||||
loadConfig(SecurityMatcherBuilderBeanConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
setup();
|
||||
this.request.setServletPath("");
|
||||
this.request.setRequestURI("/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityMatchersWhenBuilderBeanWithBasePathAndRawStringsThenHonorsBasePath() throws Exception {
|
||||
loadConfig(SecurityMatchersBuilderBeanConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
setup();
|
||||
this.request.setServletPath("");
|
||||
this.request.setRequestURI("/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityMatchersWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception {
|
||||
loadConfig(MultiMvcMatcherInLambdaConfig.class);
|
||||
@@ -430,6 +458,83 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@Import(UsersConfig.class)
|
||||
static class SecurityMatcherBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.securityMatcher("/path")
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().denyAll());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping("/path")
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@Import(UsersConfig.class)
|
||||
static class SecurityMatchersBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.securityMatchers((matchers) -> matchers
|
||||
.requestMatchers("/path")
|
||||
)
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().denyAll()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping("/path")
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class UsersConfig {
|
||||
|
||||
|
||||
+35
@@ -35,6 +35,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
@@ -127,6 +128,17 @@ public class LogoutConfigurerTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void logoutWhenBuilderBeanWithBasePathThenLogoutUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(LogoutBuilderBeanConfig.class).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/logout").with(csrf()))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// SEC-2311
|
||||
@Test
|
||||
public void logoutWhenGetRequestAndCsrfDisabledThenRedirectsToLogin() throws Exception {
|
||||
@@ -524,6 +536,29 @@ public class LogoutConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class LogoutBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CsrfDisabledConfig {
|
||||
|
||||
+34
@@ -26,6 +26,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@@ -66,6 +67,16 @@ public class PasswordManagementConfigurerTests {
|
||||
.andExpect(redirectedUrl("/custom-change-password-page"));
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void changePasswordWhenBuilderBeanWithBasePathThenChangePasswordUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(PasswordManagementBuilderBeanConfig.class).autowire();
|
||||
|
||||
this.mvc.perform(get("/.well-known/change-password"))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/change-password"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingNullChangePasswordPage() {
|
||||
PasswordManagementConfigurer configurer = new PasswordManagementConfigurer();
|
||||
@@ -102,6 +113,29 @@ public class PasswordManagementConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PasswordManagementBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
return http
|
||||
.passwordManagement(withDefaults())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PasswordManagementWithCustomChangePasswordPageConfig {
|
||||
|
||||
+41
@@ -34,6 +34,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
|
||||
@@ -185,6 +186,21 @@ public class RequestCacheConfigurerTests {
|
||||
this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void getWhenBuilderBeanWithBasePathThenSavedRequestMatcherIgnoresBasePath() throws Exception {
|
||||
this.spring.register(RequestCacheBuilderBeanConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
|
||||
// @formatter:off
|
||||
MockHttpSession session = (MockHttpSession) this.mvc.perform(request)
|
||||
.andExpect(redirectedUrl("/login"))
|
||||
.andReturn()
|
||||
.getRequest()
|
||||
.getSession();
|
||||
// @formatter:on
|
||||
this.mvc.perform(formLogin(session)).andExpect(RequestCacheResultMatcher.redirectToCachedRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
@@ -401,6 +417,31 @@ public class RequestCacheConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.formLogin(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDisabledConfig {
|
||||
|
||||
+69
@@ -43,9 +43,16 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
@@ -58,6 +65,7 @@ import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
@@ -257,6 +265,24 @@ public class WebAuthnConfigurerTests {
|
||||
.andExpect(content().string(expectedBody));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
|
||||
this.spring.register(DeleteCredentialConfiguration.class).autowire();
|
||||
this.mvc
|
||||
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
|
||||
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
|
||||
this.spring.register(DeleteCredentialConfiguration.class).autowire();
|
||||
this.mvc
|
||||
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
|
||||
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class ConfigCredentialCreationOptionsRepository {
|
||||
@@ -475,4 +501,47 @@ public class WebAuthnConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class DeleteCredentialConfiguration {
|
||||
|
||||
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
|
||||
|
||||
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
|
||||
return mock(WebAuthnRelyingPartyOperations.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserCredentialRepository userCredentialRepository() {
|
||||
MapUserCredentialRepository repository = new MapUserCredentialRepository();
|
||||
repository.save(TestCredentialRecords.userCredential().build());
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PublicKeyCredentialUserEntityRepository userEntityRepository() {
|
||||
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
|
||||
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(USER_ENTITY_ID)
|
||||
.displayName("User")
|
||||
.build());
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+38
@@ -56,6 +56,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.context.DelegatingApplicationListener;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -440,6 +441,18 @@ public class OAuth2LoginConfigurerTests {
|
||||
then(redirectStrategy).should().sendRedirect(any(), any(), anyString());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void oauth2LoginWhenBuilderBeanWithBasePathThenLoginProcessingUrlIgnoresBasePath() throws Exception {
|
||||
loadConfig(OAuth2LoginBuilderBeanConfig.class);
|
||||
String requestUri = "/login/oauth2/code/google";
|
||||
this.request = get(requestUri).build();
|
||||
this.request.setParameter("code", "code123");
|
||||
this.request.setParameter("state", "state123");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.response.getRedirectedUrl()).endsWith("/login?error");
|
||||
}
|
||||
|
||||
// gh-5347
|
||||
@Test
|
||||
public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception {
|
||||
@@ -788,6 +801,31 @@ public class OAuth2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginBuilderBeanConfig extends CommonSecurityFilterChainConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2Login((login) -> login
|
||||
.clientRegistrationRepository(
|
||||
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)));
|
||||
// @formatter:on
|
||||
return super.configureFilterChain(http);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginConfigFormLogin extends CommonSecurityFilterChainConfig {
|
||||
|
||||
+140
-4
@@ -79,6 +79,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationValidator;
|
||||
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
@@ -411,6 +412,102 @@ public class OAuth2ClientRegistrationTests {
|
||||
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenProtocolRelativeRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["//client.example.com/path"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenJavascriptSchemeRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["javascript:alert(document.cookie)"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenDataSchemeRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["data:text/html,<h1>content</h1>"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["https://client.example.com"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"jwks_uri": "http://169.254.169.254/keys",
|
||||
"token_endpoint_auth_method": "private_key_jwt"
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenArbitraryScopeThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["https://client.example.com"],
|
||||
"grant_types": ["client_credentials"],
|
||||
"scope": "read write"
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
private int requestWhenInvalidClientMetadataThenBadRequest(String json) throws Exception {
|
||||
String clientRegistrationScope = "client.create";
|
||||
// @formatter:off
|
||||
RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-" + System.nanoTime())
|
||||
.clientId("client-registrar-" + System.nanoTime())
|
||||
.clientSecret("{noop}secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.scope(clientRegistrationScope)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.registeredClientRepository.save(clientRegistrar);
|
||||
|
||||
MvcResult tokenResult = this.mvc
|
||||
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
|
||||
.with(httpBasic(clientRegistrar.getClientId(), "secret")))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
OAuth2AccessToken accessToken = readAccessTokenResponse(tokenResult.getResponse()).getAccessToken();
|
||||
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setBearerAuth(accessToken.getTokenValue());
|
||||
|
||||
MvcResult registerResult = this.mvc
|
||||
.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andReturn();
|
||||
return registerResult.getResponse().getStatus();
|
||||
}
|
||||
|
||||
private OAuth2ClientRegistration registerClient(OAuth2ClientRegistration clientRegistration) throws Exception {
|
||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||
|
||||
@@ -496,6 +593,17 @@ public class OAuth2ClientRegistrationTests {
|
||||
return clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
|
||||
}
|
||||
|
||||
private static Consumer<List<AuthenticationProvider>> scopePermissiveValidatorCustomizer() {
|
||||
return (authenticationProviders) -> authenticationProviders.forEach((authenticationProvider) -> {
|
||||
if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
|
||||
provider.setAuthenticationValidator(
|
||||
OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
||||
@@ -512,7 +620,7 @@ public class OAuth2ClientRegistrationTests {
|
||||
.clientRegistrationRequestConverter(authenticationConverter)
|
||||
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.authenticationProviders(authenticationProvidersConsumer)
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(authenticationProvidersConsumer))
|
||||
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
||||
.errorResponseHandler(authenticationFailureHandler)
|
||||
)
|
||||
@@ -539,7 +647,7 @@ public class OAuth2ClientRegistrationTests {
|
||||
authorizationServer
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.authenticationProviders(configureClientRegistrationConverters())
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
@@ -577,7 +685,7 @@ public class OAuth2ClientRegistrationTests {
|
||||
authorizationServer
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.authenticationProviders(configureClientRegistrationConverters())
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
@@ -614,6 +722,7 @@ public class OAuth2ClientRegistrationTests {
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.openRegistrationAllowed(true)
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer())
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
@@ -627,6 +736,30 @@ public class OAuth2ClientRegistrationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DefaultValidatorConfiguration extends AuthorizationServerConfiguration {
|
||||
|
||||
// Override with Customizer.withDefaults() so the default (strict)
|
||||
// OAuth2ClientRegistrationAuthenticationValidator is in effect.
|
||||
// @formatter:off
|
||||
@Bean
|
||||
@Override
|
||||
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.oauth2AuthorizationServer((authorizationServer) ->
|
||||
authorizationServer
|
||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
authorize.anyRequest().authenticated()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class AuthorizationServerConfiguration {
|
||||
@@ -637,7 +770,10 @@ public class OAuth2ClientRegistrationTests {
|
||||
http
|
||||
.oauth2AuthorizationServer((authorizationServer) ->
|
||||
authorizationServer
|
||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer())
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
authorize.anyRequest().authenticated()
|
||||
|
||||
+170
-4
@@ -97,6 +97,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcClientR
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationValidator;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||
@@ -545,6 +546,129 @@ public class OidcClientRegistrationTests {
|
||||
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenProtocolRelativeRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["//client.example.com/path"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenJavascriptSchemeRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["javascript:alert(document.cookie)"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenDataSchemeRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["data:text/html,<h1>content</h1>"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenJavascriptSchemePostLogoutRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["https://client.example.com"],
|
||||
"post_logout_redirect_uris": ["javascript:alert(document.cookie)"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenDataSchemePostLogoutRedirectUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["https://client.example.com"],
|
||||
"post_logout_redirect_uris": ["data:text/html,<h1>content</h1>"],
|
||||
"grant_types": ["authorization_code"]
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["https://client.example.com"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"jwks_uri": "http://169.254.169.254/keys",
|
||||
"token_endpoint_auth_method": "private_key_jwt"
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenArbitraryScopeThenBadRequest() throws Exception {
|
||||
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||
{
|
||||
"client_name": "client-name",
|
||||
"redirect_uris": ["https://client.example.com"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"scope": "read write"
|
||||
}
|
||||
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
private int requestWhenInvalidClientMetadataThenBadRequest(String json) throws Exception {
|
||||
String clientRegistrationScope = "client.create";
|
||||
String clientId = "client-registrar-" + System.nanoTime();
|
||||
// @formatter:off
|
||||
RegisteredClient clientRegistrar = RegisteredClient.withId(clientId)
|
||||
.clientId(clientId)
|
||||
.clientSecret("{noop}secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.scope(clientRegistrationScope)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.registeredClientRepository.save(clientRegistrar);
|
||||
|
||||
MvcResult tokenResult = this.mvc
|
||||
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
|
||||
.with(httpBasic(clientId, "secret")))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
OAuth2AccessToken accessToken = readAccessTokenResponse(tokenResult.getResponse()).getAccessToken();
|
||||
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setBearerAuth(accessToken.getTokenValue());
|
||||
|
||||
MvcResult registerResult = this.mvc
|
||||
.perform(post(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andReturn();
|
||||
return registerResult.getResponse().getStatus();
|
||||
}
|
||||
|
||||
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||
|
||||
@@ -642,6 +766,18 @@ public class OidcClientRegistrationTests {
|
||||
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
||||
}
|
||||
|
||||
private static Consumer<List<AuthenticationProvider>> scopePermissiveValidatorCustomizer() {
|
||||
return (authenticationProviders) -> authenticationProviders.forEach((authenticationProvider) -> {
|
||||
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
|
||||
provider.setAuthenticationValidator(
|
||||
OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR.andThen(
|
||||
OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
||||
@@ -660,7 +796,7 @@ public class OidcClientRegistrationTests {
|
||||
.clientRegistrationRequestConverter(authenticationConverter)
|
||||
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.authenticationProviders(authenticationProvidersConsumer)
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(authenticationProvidersConsumer))
|
||||
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
||||
.errorResponseHandler(authenticationFailureHandler)
|
||||
)
|
||||
@@ -690,7 +826,7 @@ public class OidcClientRegistrationTests {
|
||||
oidc
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.authenticationProviders(configureClientRegistrationConverters())
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -731,7 +867,7 @@ public class OidcClientRegistrationTests {
|
||||
oidc
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.authenticationProviders(configureClientRegistrationConverters())
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -755,6 +891,33 @@ public class OidcClientRegistrationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DefaultValidatorConfiguration extends AuthorizationServerConfiguration {
|
||||
|
||||
// Override with Customizer.withDefaults() so the default (strict)
|
||||
// OidcClientRegistrationAuthenticationValidator is in effect.
|
||||
// @formatter:off
|
||||
@Bean
|
||||
@Override
|
||||
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.oauth2AuthorizationServer((authorizationServer) ->
|
||||
authorizationServer
|
||||
.oidc((oidc) ->
|
||||
oidc
|
||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
authorize.anyRequest().authenticated()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class AuthorizationServerConfiguration {
|
||||
@@ -767,7 +930,10 @@ public class OidcClientRegistrationTests {
|
||||
authorizationServer
|
||||
.oidc((oidc) ->
|
||||
oidc
|
||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||
.clientRegistrationEndpoint((clientRegistration) ->
|
||||
clientRegistration
|
||||
.authenticationProviders(scopePermissiveValidatorCustomizer())
|
||||
)
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
|
||||
+15
-7
@@ -60,6 +60,7 @@ import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
@@ -210,7 +211,8 @@ public class OidcTests {
|
||||
registeredClient);
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
|
||||
.with(user("user").roles("A", "B")))
|
||||
.with(user("user").roles("A", "B")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
@@ -270,7 +272,8 @@ public class OidcTests {
|
||||
registeredClient);
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
|
||||
.with(user("user").roles("A", "B")))
|
||||
.with(user("user").roles("A", "B")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
@@ -335,7 +338,8 @@ public class OidcTests {
|
||||
registeredClient);
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI)).queryParams(authorizationRequestParameters)
|
||||
.with(user("user")))
|
||||
.with(user("user")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
@@ -388,7 +392,8 @@ public class OidcTests {
|
||||
registeredClient1);
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
|
||||
.with(user("user1")))
|
||||
.with(user("user1")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
@@ -424,7 +429,8 @@ public class OidcTests {
|
||||
authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient2);
|
||||
mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
|
||||
.with(user("user2")))
|
||||
.with(user("user2")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
@@ -497,7 +503,8 @@ public class OidcTests {
|
||||
registeredClient);
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
|
||||
.with(user("user")))
|
||||
.with(user("user")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
@@ -537,7 +544,8 @@ public class OidcTests {
|
||||
registeredClient);
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
|
||||
.with(user("user")))
|
||||
.with(user("user")
|
||||
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
|
||||
+49
@@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -98,6 +99,19 @@ public class OneTimeTokenLoginConfigurerTests {
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
void oneTimeTokenWhenBuilderBeanWithBasePathThenGenerateAndLoginUrlsIgnoreBasePath() throws Exception {
|
||||
this.spring.register(OneTimeTokenBuilderBeanConfig.class).autowire();
|
||||
this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf()))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
|
||||
|
||||
String token = getLastToken().getTokenValue();
|
||||
|
||||
this.mvc.perform(post("/login/ott").param("token", token).with(csrf()))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() throws Exception {
|
||||
this.spring.register(OneTimeTokenDifferentUrlsConfig.class).autowire();
|
||||
@@ -296,6 +310,41 @@ public class OneTimeTokenLoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@Import(UserDetailsServiceConfig.class)
|
||||
static class OneTimeTokenBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http,
|
||||
OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oneTimeTokenLogin((ott) -> ott
|
||||
.tokenGenerationSuccessHandler(ottSuccessHandler)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
|
||||
return new TestOneTimeTokenGenerationSuccessHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@Import(UserDetailsServiceConfig.class)
|
||||
|
||||
+36
@@ -57,6 +57,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
@@ -244,6 +245,14 @@ public class Saml2LoginConfigurerTests {
|
||||
performSaml2Login("ROLE_AUTH_MANAGER");
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void saml2LoginWhenBuilderBeanWithBasePathThenAuthenticateUriIgnoresBasePath() throws Exception {
|
||||
this.spring.register(Saml2LoginBuilderBeanConfig.class).autowire();
|
||||
MvcResult result = this.mvc.perform(get("/saml2/authenticate/registration-id")).andReturn();
|
||||
assertThat(result.getResponse().getRedirectedUrl()).contains("SAMLRequest");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticationRequestWhenAuthenticationRequestResolverBeanThenUses() throws Exception {
|
||||
this.spring.register(CustomAuthenticationRequestResolverBean.class).autowire();
|
||||
@@ -522,6 +531,33 @@ public class Saml2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
static class Saml2LoginBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authz) -> authz
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.saml2Login(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
|
||||
+38
@@ -49,6 +49,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.saml2.core.Saml2Utils;
|
||||
@@ -196,6 +197,17 @@ public class Saml2LogoutConfigurerTests {
|
||||
verify(logoutHandler).logout(any(), any(), any());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void saml2LogoutWhenBuilderBeanWithBasePathThenLogoutUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(Saml2LogoutBuilderBeanConfig.class).autowire();
|
||||
MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf()))
|
||||
.andExpect(status().isFound())
|
||||
.andReturn();
|
||||
String location = result.getResponse().getHeader("Location");
|
||||
assertThat(location).startsWith("https://ap.example.org/logout/saml2/request");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception {
|
||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
||||
@@ -577,6 +589,32 @@ public class Saml2LogoutConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
static class Saml2LogoutBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.saml2Login(withDefaults())
|
||||
.saml2Logout(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
|
||||
+35
@@ -30,6 +30,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.saml2.provider.service.metadata.OpenSaml5MetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
|
||||
@@ -124,6 +125,15 @@ public class Saml2MetadataConfigurerTests {
|
||||
this.mvc.perform(get("/saml2/metadata")).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
void saml2MetadataWhenBuilderBeanWithBasePathThenMetadataUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(MetadataBuilderBeanConfig.class).autowire();
|
||||
this.mvc.perform(get("/saml/metadata"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("md:EntityDescriptor")));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Import(RelyingPartyRegistrationConfig.class)
|
||||
@@ -141,6 +151,31 @@ public class Saml2MetadataConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Import(RelyingPartyRegistrationConfig.class)
|
||||
static class MetadataBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata"));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Import(RelyingPartyRegistrationConfig.class)
|
||||
|
||||
+25
@@ -28,14 +28,17 @@ import jakarta.servlet.http.HttpSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.session.SessionLimit;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -150,6 +153,16 @@ public class HttpHeadersConfigTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenHeadersEagerlyConfiguredThenHeadersAreWritten() throws Exception {
|
||||
this.spring.configLocations(this.xml("HeadersEagerlyConfigured")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/").secure(true))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(includesDefaults());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenFrameOptionsConfiguredThenIncludesHeader() throws Exception {
|
||||
Map<String, String> headers = new HashMap<>(defaultHeaders);
|
||||
@@ -955,6 +968,18 @@ public class HttpHeadersConfigTests {
|
||||
|
||||
}
|
||||
|
||||
public static class EagerHeadersBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof HeaderWriterFilter headerWriterFilter) {
|
||||
headerWriterFilter.setShouldWriteHeadersEagerly(true);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomSessionLimit implements SessionLimit {
|
||||
|
||||
@Override
|
||||
|
||||
+72
@@ -314,6 +314,78 @@ public class InterceptUrlConfigTests {
|
||||
.autowire());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingDefaultMatcherAndServletPathThenAuthorizesRequestsAccordingly() throws Exception {
|
||||
this.spring.configLocations(this.xml("DefaultMatcherServletPath")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/spring/path").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/path").with(userCredentials()))
|
||||
.andExpect(status().isOk());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingDefaultMatcherAndServletPathAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||
throws Exception {
|
||||
this.spring.configLocations(this.xml("DefaultMatcherServletPathAuthorizationManager")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/spring/path").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/path").with(userCredentials()))
|
||||
.andExpect(status().isOk());
|
||||
// @formatter:on
|
||||
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingRegexMatcherThenAuthorizesRequestsAccordingly() throws Exception {
|
||||
this.spring.configLocations(this.xml("RegexMatcher")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/path").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/other").with(userCredentials()))
|
||||
.andExpect(status().isNotFound());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingRegexMatcherAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||
throws Exception {
|
||||
this.spring.configLocations(this.xml("RegexMatcherAuthorizationManager")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/path").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/other").with(userCredentials()))
|
||||
.andExpect(status().isNotFound());
|
||||
// @formatter:on
|
||||
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingCiRegexMatcherThenAuthorizesRequestsAccordingly() throws Exception {
|
||||
this.spring.configLocations(this.xml("CiRegexMatcher")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/path").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/PATH").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingCiRegexMatcherAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||
throws Exception {
|
||||
this.spring.configLocations(this.xml("CiRegexMatcherAuthorizationManager")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/path").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mvc.perform(get("/PATH").with(userCredentials()))
|
||||
.andExpect(status().isForbidden());
|
||||
// @formatter:on
|
||||
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUsingFilterAllDispatcherTypesAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||
throws Exception {
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http auto-config="true">
|
||||
<headers/>
|
||||
<intercept-url pattern="/**" access="permitAll"/>
|
||||
</http>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.HttpHeadersConfigTests.EagerHeadersBeanPostProcessor"/>
|
||||
|
||||
<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http request-matcher="ciRegex" use-authorization-manager="false">
|
||||
<intercept-url pattern="\A/PATH\Z" access="denyAll"/>
|
||||
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http request-matcher="ciRegex">
|
||||
<intercept-url pattern="\A/PATH\Z" access="denyAll"/>
|
||||
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
+3
@@ -26,8 +26,11 @@
|
||||
|
||||
<http use-authorization-manager="false">
|
||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||
<intercept-url pattern="/**" access="permitAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
|
||||
+3
@@ -26,8 +26,11 @@
|
||||
|
||||
<http>
|
||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||
<intercept-url pattern="/**" access="permitAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http request-matcher="regex" use-authorization-manager="false">
|
||||
<intercept-url pattern="\A/path\Z" access="denyAll"/>
|
||||
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http request-matcher="regex">
|
||||
<intercept-url pattern="\A/path\Z" access="denyAll"/>
|
||||
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+39
-4
@@ -97,6 +97,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
|
||||
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
|
||||
|
||||
private boolean alwaysPerformAdditionalChecksOnUser = true;
|
||||
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
|
||||
|
||||
private static final String AUTHORITY = FactorGrantedAuthority.PASSWORD_AUTHORITY;
|
||||
@@ -154,8 +156,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
|
||||
}
|
||||
try {
|
||||
this.preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
if (!cacheWasUsed) {
|
||||
@@ -165,8 +166,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
// we're using latest data (i.e. not from the cache)
|
||||
cacheWasUsed = false;
|
||||
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
|
||||
this.preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
}
|
||||
this.postAuthenticationChecks.check(user);
|
||||
if (!cacheWasUsed) {
|
||||
@@ -179,6 +179,25 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
return createSuccessAuthentication(principalToReturn, authentication, user);
|
||||
}
|
||||
|
||||
private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) {
|
||||
try {
|
||||
this.preAuthenticationChecks.check(user);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
if (!this.alwaysPerformAdditionalChecksOnUser) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
additionalAuthenticationChecks(user, authentication);
|
||||
}
|
||||
catch (AuthenticationException ignored) {
|
||||
// preserve the original failed check
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
additionalAuthenticationChecks(user, authentication);
|
||||
}
|
||||
|
||||
private String determineUsername(Authentication authentication) {
|
||||
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
|
||||
}
|
||||
@@ -324,6 +343,22 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
this.postAuthenticationChecks = postAuthenticationChecks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to always perform the additional checks on the user, even if the
|
||||
* pre-authentication checks fail. This is useful to ensure that regardless of the
|
||||
* state of the user account, authentication takes the same amount of time to
|
||||
* complete.
|
||||
*
|
||||
* <p>
|
||||
* For applications that rely on the additional checks running only once should set
|
||||
* this value to {@code false}
|
||||
* @param alwaysPerformAdditionalChecksOnUser
|
||||
* @since 5.7.23
|
||||
*/
|
||||
public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) {
|
||||
this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser;
|
||||
}
|
||||
|
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||
this.authoritiesMapper = authoritiesMapper;
|
||||
}
|
||||
|
||||
+5
-3
@@ -153,7 +153,9 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
||||
return null;
|
||||
}
|
||||
OneTimeToken token = tokens.get(0);
|
||||
deleteOneTimeToken(token);
|
||||
if (deleteOneTimeToken(token) == 0) {
|
||||
return null;
|
||||
}
|
||||
if (isExpired(token)) {
|
||||
return null;
|
||||
}
|
||||
@@ -171,11 +173,11 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
||||
return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper);
|
||||
}
|
||||
|
||||
private void deleteOneTimeToken(OneTimeToken oneTimeToken) {
|
||||
private int deleteOneTimeToken(OneTimeToken oneTimeToken) {
|
||||
List<SqlParameterValue> parameters = List
|
||||
.of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
|
||||
return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
|
||||
}
|
||||
|
||||
private @Nullable ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,6 +30,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class FactorAuthorizationDecision implements AuthorizationResult {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -245342816437885039L;
|
||||
|
||||
private final List<RequiredFactorError> factorErrors;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -40,7 +42,10 @@ import org.springframework.util.Assert;
|
||||
* @author Rob Winch
|
||||
* @since 7.0
|
||||
*/
|
||||
public final class RequiredFactor {
|
||||
public final class RequiredFactor implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 295501208651764485L;
|
||||
|
||||
private final String authority;
|
||||
|
||||
|
||||
+6
-1
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
@@ -27,7 +29,10 @@ import org.springframework.util.Assert;
|
||||
* @author Rob Winch
|
||||
* @since 7.0
|
||||
*/
|
||||
public class RequiredFactorError {
|
||||
public class RequiredFactorError implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1946221547278528901L;
|
||||
|
||||
private final RequiredFactor requiredFactor;
|
||||
|
||||
|
||||
+1
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.authentication;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class NonBuildableAuthenticationToken extends TestingAuthenticationToken {
|
||||
|
||||
public NonBuildableAuthenticationToken(String user, String password, String... authorities) {
|
||||
|
||||
+41
-6
@@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
@@ -64,6 +66,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -422,12 +425,10 @@ public class DaoAuthenticationProviderTests {
|
||||
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
|
||||
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
|
||||
* SEC-2056 is fixed.
|
||||
*/
|
||||
public void IGNOREtestSec2056() {
|
||||
// SEC-2056
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
|
||||
public void testSec2056() {
|
||||
UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod",
|
||||
"koala");
|
||||
UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken
|
||||
@@ -460,6 +461,40 @@ public class DaoAuthenticationProviderTests {
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
// related to SEC-2056
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
|
||||
public void testDisabledUserTiming() {
|
||||
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
|
||||
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
|
||||
users.password = encoder.encode((CharSequence) user.getCredentials());
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(users);
|
||||
provider.setPasswordEncoder(encoder);
|
||||
int sampleSize = 100;
|
||||
List<Long> enabledTimes = new ArrayList<>(sampleSize);
|
||||
for (int i = 0; i < sampleSize; i++) {
|
||||
long start = System.currentTimeMillis();
|
||||
provider.authenticate(user);
|
||||
enabledTimes.add(System.currentTimeMillis() - start);
|
||||
}
|
||||
UserDetailsChecker preChecks = mock(UserDetailsChecker.class);
|
||||
willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class));
|
||||
provider.setPreAuthenticationChecks(preChecks);
|
||||
List<Long> disabledTimes = new ArrayList<>(sampleSize);
|
||||
for (int i = 0; i < sampleSize; i++) {
|
||||
long start = System.currentTimeMillis();
|
||||
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user));
|
||||
disabledTimes.add(System.currentTimeMillis() - start);
|
||||
}
|
||||
double enabledAvg = avg(enabledTimes);
|
||||
double disabledAvg = avg(disabledTimes);
|
||||
assertThat(Math.abs(disabledAvg - enabledAvg) <= 3)
|
||||
.withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average "
|
||||
+ enabledAvg)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
private double avg(List<Long> counts) {
|
||||
return counts.stream().mapToLong(Long::longValue).average().orElse(0);
|
||||
}
|
||||
|
||||
+26
@@ -21,19 +21,24 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.PreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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;
|
||||
|
||||
@@ -145,6 +150,27 @@ class JdbcOneTimeTokenServiceTests {
|
||||
assertThat(consumedOneTimeToken).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception {
|
||||
// Simulates a concurrent consume: SELECT finds the token but DELETE affects
|
||||
// 0 rows because another caller already consumed it.
|
||||
JdbcOperations jdbcOperations = mock(JdbcOperations.class);
|
||||
Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES);
|
||||
OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired);
|
||||
given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class),
|
||||
ArgumentMatchers.<RowMapper<OneTimeToken>>any()))
|
||||
.willReturn(List.of(token));
|
||||
given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0);
|
||||
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations);
|
||||
try {
|
||||
OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE));
|
||||
assertThat(consumed).isNull();
|
||||
}
|
||||
finally {
|
||||
service.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeWhenTokenIsExpiredThenReturnNull() {
|
||||
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
|
||||
|
||||
@@ -11,13 +11,16 @@ dependencies {
|
||||
def springFrameworkVersion = project.property("springFrameworkVersion")
|
||||
def reactorVersion = project.property("reactorVersion")
|
||||
def springDataVersion = project.property("springDataVersion")
|
||||
def micrometerVersion = project.property("micrometerVersion")
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:$reactorVersion")
|
||||
api platform("org.springframework.data:spring-data-bom:$springDataVersion")
|
||||
api platform("io.micrometer:micrometer-bom:$micrometerVersion")
|
||||
} else {
|
||||
api platform(libs.org.springframework.spring.framework.bom)
|
||||
api platform(libs.io.projectreactor.reactor.bom)
|
||||
api platform(libs.org.springframework.data.spring.data.bom)
|
||||
api platform(libs.io.micrometer.micrometer.bom)
|
||||
}
|
||||
api platform(libs.io.rsocket.rsocket.bom)
|
||||
api platform(libs.org.junit.junit.bom)
|
||||
|
||||
@@ -376,7 +376,9 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -386,7 +388,9 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -452,8 +456,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -463,8 +469,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -479,8 +487,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -490,8 +500,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -506,11 +518,13 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(ES512);
|
||||
}).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -520,12 +534,14 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms {
|
||||
it.add(RS512)
|
||||
it.add(ES512)
|
||||
}
|
||||
.build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -81,6 +81,12 @@ Xml::
|
||||
|
||||
and no authorization changes are necessary since it simply adjusts the `LogoutFilter`.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The URI passed to `logoutUrl` (and to `logoutSuccessUrl`) is matched and redirected to literally.
|
||||
If you want either URI to live under your application servlet's base path, include that prefix explicitly — for example, `logoutUrl("/api/logout")`.
|
||||
====
|
||||
|
||||
[[permit-logout-endpoints]]
|
||||
However, if you stand up your own logout success endpoint (or in a rare case, <<creating-custom-logout-endpoint, your own logout endpoint>>), say using {spring-framework-reference-url}web.html#spring-web[Spring MVC], you will need to permit it in Spring Security.
|
||||
This is because Spring MVC processes your request after Spring Security does.
|
||||
|
||||
@@ -252,6 +252,13 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSucc
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The URI passed to `generateTokenUrl` is matched literally.
|
||||
If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `generateTokenUrl("/api/ott/generate")`.
|
||||
The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page.
|
||||
====
|
||||
|
||||
[[changing-submit-page-url]]
|
||||
== Changing the Default Submit Page URL
|
||||
|
||||
|
||||
@@ -155,6 +155,15 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The URI passed to `loginPage` (and to `loginProcessingUrl`, if set separately) is matched and redirected to literally.
|
||||
If your custom login page is served by an application servlet mapped under a base path, include that prefix in the URI you pass to the DSL.
|
||||
For example, if your dispatcher is at `/api/*`:
|
||||
|
||||
include-code::./FormLoginServletPathConfiguration[tag=loginPage,indent=0]
|
||||
====
|
||||
|
||||
[[servlet-authentication-form-custom-html]]
|
||||
When the login page is specified in the Spring Security configuration, you are responsible for rendering the page.
|
||||
// FIXME: default login page rendered by Spring Security
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
Once you have got an application that is xref:servlet/authentication/index.adoc[authenticating requests], it is important to consider how that resulting authentication will be persisted and restored on future requests.
|
||||
|
||||
This is done automatically by default, so no additional code is necessary, though it is important to know what `requireExplicitSave` means in `HttpSecurity`.
|
||||
This is done automatically by default.
|
||||
If you have a custom filter or controller that is setting the security context, you will need to use a `SecurityContextRepository` to persist it across requests.
|
||||
|
||||
If you are upgrading from an older version, you may be interested in the `requireExplicitSave` setting that preserves Spring Security 5's default, though note that this is primarily for migration purposes.
|
||||
|
||||
If you like, <<how-it-works-requireexplicitsave,you can read more about what requireExplicitSave is doing>> or <<requireexplicitsave,why it's important>>. Otherwise, in most cases you are done with this section.
|
||||
|
||||
|
||||
@@ -642,6 +642,8 @@ This is because Spring Security requires all URIs to be absolute (minus the cont
|
||||
There are several other components that create request matchers for you like {spring-boot-api-url}org/springframework/boot/security/autoconfigure/web/servlet/PathRequest.html[`PathRequest#toStaticResources#atCommonLocations`]
|
||||
=====
|
||||
|
||||
If most of your authorization rules sit under the same servlet path, you can xref:servlet/integrations/mvc.adoc#mvc-requestmatcher[publish a `PathPatternRequestMatcher.Builder` bean] with that base path; Spring Security then applies it to the string overloads of `authorizeHttpRequests((authorize) -> authorize.requestMatchers(...))`, `HttpSecurity#securityMatcher(...)` / `#securityMatchers(...)`, and `WebSecurityCustomizer#ignoring().requestMatchers(...)`.
|
||||
|
||||
[[match-by-custom]]
|
||||
=== Using a Custom Matcher
|
||||
|
||||
|
||||
@@ -1382,12 +1382,15 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
|
||||
public class MyPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
|
||||
@Override
|
||||
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MyPostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
|
||||
@Override
|
||||
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
|
||||
// ... authorization logic
|
||||
@@ -1400,11 +1403,14 @@ Kotlin::
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
|
||||
class MyPreAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocation> {
|
||||
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class MyPostAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocationResult> {
|
||||
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
|
||||
// ... authorization logic
|
||||
}
|
||||
@@ -1427,13 +1433,13 @@ Java::
|
||||
class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preAuthorize(MyAuthorizationManager manager) {
|
||||
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postAuthorize(MyAuthorizationManager manager) {
|
||||
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
|
||||
}
|
||||
}
|
||||
@@ -1446,15 +1452,15 @@ Kotlin::
|
||||
@Configuration
|
||||
@EnableMethodSecurity(prePostEnabled = false)
|
||||
class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun preAuthorize(manager: MyAuthorizationManager) : Advisor {
|
||||
fun preAuthorize(manager: MyPreAuthorizeAuthorizationManager): Advisor {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun postAuthorize(manager: MyAuthorizationManager) : Advisor {
|
||||
fun postAuthorize(manager: MyPostAuthorizeAuthorizationManager): Advisor {
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
|
||||
}
|
||||
}
|
||||
@@ -1471,13 +1477,13 @@ Xml::
|
||||
<bean id="preAuthorize"
|
||||
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
|
||||
factory-method="preAuthorize">
|
||||
<constructor-arg ref="myAuthorizationManager"/>
|
||||
<constructor-arg ref="myPreAuthorizeAuthorizationManager"/>
|
||||
</bean>
|
||||
|
||||
<bean id="postAuthorize"
|
||||
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
|
||||
factory-method="postAuthorize">
|
||||
<constructor-arg ref="myAuthorizationManager"/>
|
||||
<constructor-arg ref="myPostAuthorizeAuthorizationManager"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
@@ -1487,6 +1493,8 @@ Xml::
|
||||
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
|
||||
====
|
||||
|
||||
You can also implement `MethodAuthorizationDeniedHandler` in the same manager class to override the default exception-handling behavior.
|
||||
|
||||
[[customizing-expression-handling]]
|
||||
=== Customizing Expression Handling
|
||||
|
||||
|
||||
@@ -519,7 +519,9 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -529,7 +531,9 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -595,8 +599,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -606,8 +612,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -622,8 +630,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -633,8 +643,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -649,11 +661,13 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(ES512);
|
||||
}).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -663,11 +677,13 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms {
|
||||
it.add(RS512)
|
||||
it.add(ES512)
|
||||
}.build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.11",
|
||||
"antora": "3.2.0-alpha.12",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@antora/collector-extension": "1.0.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
"@springio/antora-extensions": "1.14.12",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ plugins {
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
node {
|
||||
version = "24.15.0"
|
||||
}
|
||||
|
||||
antora {
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
environment = [
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.passwords.servletauthenticationform;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class FormLoginServletPathConfiguration {
|
||||
|
||||
// tag::loginPage[]
|
||||
@Bean
|
||||
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin((form) -> form
|
||||
.loginPage("/api/login")
|
||||
.loginProcessingUrl("/api/login")
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::loginPage[]
|
||||
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.passwords.servletauthenticationform
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class FormLoginServletPathConfiguration {
|
||||
|
||||
// tag::loginPage[]
|
||||
@Bean
|
||||
fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin {
|
||||
loginPage = "/api/login"
|
||||
loginProcessingUrl = "/api/login"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
// @formatter:on
|
||||
}
|
||||
// end::loginPage[]
|
||||
|
||||
}
|
||||
@@ -14,3 +14,5 @@
|
||||
^http://schemas.openid.net/event/backchannel-logout
|
||||
^http://host.docker.internal:8090/back-channel/logout
|
||||
^http://host.docker.internal:8090/logout
|
||||
^http://169.254.169.254/keys
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=4.0.0-SNAPSHOT
|
||||
version=7.0.4
|
||||
version=7.0.7-SNAPSHOT
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+13
-11
@@ -1,22 +1,23 @@
|
||||
[versions]
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-micrometer = "1.16.6"
|
||||
io-rsocket = "1.1.5"
|
||||
io-spring-javaformat = "0.0.47"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.2.0"
|
||||
org-apache-maven-resolver = "1.9.27"
|
||||
org-aspectj = "1.9.25.1"
|
||||
org-bouncycastle = "1.80"
|
||||
org-bouncycastle = "1.80.2"
|
||||
org-eclipse-jetty = "11.0.26"
|
||||
org-jetbrains-kotlin = "2.2.21"
|
||||
org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml5 = "5.1.6"
|
||||
org-springframework = "7.0.6"
|
||||
org-springframework = "7.0.8"
|
||||
com-password4j = "1.8.4"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.34"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.20.2"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
@@ -28,9 +29,10 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:7.0.4"
|
||||
com-jayway-jsonpath-json-path = "com.jayway.jsonpath:json-path:2.9.0"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.4"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.14"
|
||||
io-mockk = "io.mockk:mockk:1.14.9"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.4"
|
||||
io-micrometer-micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "io-micrometer" }
|
||||
io-micrometer-micrometer-observation = { module = "io.micrometer:micrometer-observation", version.ref = "io-micrometer" }
|
||||
io-mockk = "io.mockk:mockk:1.14.11"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.6"
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
@@ -51,7 +53,7 @@ net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.70.0"
|
||||
org-htmlunit-htmlunit = "org.htmlunit:htmlunit:4.11.1"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents.client5:httpclient5:5.5.2"
|
||||
org-apache-kerby-simplekdc='org.apache.kerby:kerb-simplekdc:2.1.1'
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.14"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.16"
|
||||
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
|
||||
@@ -80,9 +82,9 @@ org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.4"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.2"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.18"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.6"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.4"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.4"
|
||||
@@ -102,7 +104,7 @@ org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanne
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.6'
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.1.RELEASE'
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.6.RELEASE'
|
||||
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
|
||||
|
||||
[plugins]
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=f1771298a70f6db5a29daf62378c4e18a17fc33c9ba6b14362e0cdf40610380d
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
||||
distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+1
@@ -226,6 +226,7 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
|
||||
return userCode != null && userCode.getToken().getTokenValue().equals(token);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
|
||||
|
||||
private final int maxSize;
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
@@ -32,6 +33,9 @@ import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
*/
|
||||
public class OAuth2AuthorizationCode extends AbstractOAuth2Token {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 3789328028057414501L;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCode} using the provided parameters.
|
||||
* @param tokenValue the token value
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -36,6 +37,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 4629166286850598162L;
|
||||
|
||||
private final String code;
|
||||
|
||||
private final String redirectUri;
|
||||
|
||||
+5
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
@@ -33,6 +35,9 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -3791188557904282453L;
|
||||
|
||||
private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -489,7 +489,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
registeredClient);
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
|
||||
&& (parameterName.equals(OAuth2ParameterNames.CLIENT_ID)
|
||||
|| parameterName.equals(OAuth2ParameterNames.STATE))) {
|
||||
|| parameterName.equals(OAuth2ParameterNames.STATE)
|
||||
|| parameterName.equals(OAuth2ParameterNames.REQUEST_URI))) {
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -293,8 +293,10 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
? authorizationCodeRequestAuthentication.getRedirectUri()
|
||||
: registeredClient.getRedirectUris().iterator().next();
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
|
||||
&& parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
|
||||
if ((error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
|
||||
|| error.getErrorCode().equals(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT))
|
||||
&& (parameterName.equals(OAuth2ParameterNames.CLIENT_ID)
|
||||
|| parameterName.equals(OAuth2ParameterNames.REDIRECT_URI))) {
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
@@ -36,6 +37,9 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
*/
|
||||
public class OAuth2ClientCredentialsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -220223451609576578L;
|
||||
|
||||
private final Set<String> scopes;
|
||||
|
||||
/**
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an
|
||||
* {@link OAuth2ClientRegistrationAuthenticationToken} and additional information and is
|
||||
* used when validating the OAuth 2.0 Client Registration Request parameters.
|
||||
*
|
||||
* @author addcontent
|
||||
* @since 7.0.5
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2ClientRegistrationAuthenticationToken
|
||||
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2ClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OAuth2ClientRegistrationAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided
|
||||
* {@link OAuth2ClientRegistrationAuthenticationToken}.
|
||||
* @param authentication the {@link OAuth2ClientRegistrationAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2ClientRegistrationAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2ClientRegistrationAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2ClientRegistrationAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2ClientRegistrationAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2ClientRegistrationAuthenticationContext}.
|
||||
* @return the {@link OAuth2ClientRegistrationAuthenticationContext}
|
||||
*/
|
||||
@Override
|
||||
public OAuth2ClientRegistrationAuthenticationContext build() {
|
||||
return new OAuth2ClientRegistrationAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+29
-37
@@ -16,12 +16,10 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -35,12 +33,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
@@ -49,7 +45,6 @@ import org.springframework.security.oauth2.server.authorization.converter.OAuth2
|
||||
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -67,8 +62,6 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public final class OAuth2ClientRegistrationAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
|
||||
|
||||
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
@@ -85,6 +78,8 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
|
||||
private boolean openRegistrationAllowed;
|
||||
|
||||
private Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
@@ -99,6 +94,7 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
|
||||
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
|
||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
this.authenticationValidator = new OAuth2ClientRegistrationAuthenticationValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -197,14 +193,35 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
this.openRegistrationAllowed = openRegistrationAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the
|
||||
* {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
|
||||
* validating specific OAuth 2.0 Client Registration Request parameters associated in
|
||||
* the {@link OAuth2ClientRegistrationAuthenticationToken}. The default authentication
|
||||
* validator is {@link OAuth2ClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw
|
||||
* {@link OAuth2AuthenticationException} if validation fails.
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the
|
||||
* {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
|
||||
* validating specific OAuth 2.0 Client Registration Request parameters
|
||||
* @since 7.0.5
|
||||
*/
|
||||
public void setAuthenticationValidator(
|
||||
Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private OAuth2ClientRegistrationAuthenticationToken registerClient(
|
||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
OAuth2ClientRegistrationAuthenticationContext authenticationContext = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(clientRegistrationAuthentication)
|
||||
.build();
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client registration request parameters");
|
||||
@@ -277,29 +294,4 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidRedirectUris(List<String> redirectUris) {
|
||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String redirectUri : redirectUris) {
|
||||
try {
|
||||
URI validRedirectUri = new URI(redirectUri);
|
||||
if (validRedirectUri.getFragment() != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A {@code Consumer} providing access to the
|
||||
* {@link OAuth2ClientRegistrationAuthenticationContext} containing an
|
||||
* {@link OAuth2ClientRegistrationAuthenticationToken} and is the default
|
||||
* {@link OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
* authentication validator} used for validating specific OAuth 2.0 Dynamic Client
|
||||
* Registration Request parameters (RFC 7591).
|
||||
*
|
||||
* <p>
|
||||
* The default implementation validates {@link OAuth2ClientRegistration#getRedirectUris()
|
||||
* redirect_uris}, {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}, and
|
||||
* {@link OAuth2ClientRegistration#getScopes() scope}. If validation fails, an
|
||||
* {@link OAuth2AuthenticationException} is thrown.
|
||||
*
|
||||
* <p>
|
||||
* Each validated field is backed by two public constants:
|
||||
* <ul>
|
||||
* <li>{@code DEFAULT_*_VALIDATOR} -- strict validation that rejects unsafe values. This
|
||||
* is the default behavior and may reject input that was previously accepted.</li>
|
||||
* <li>{@code SIMPLE_*_VALIDATOR} -- lenient validation preserving the behavior from prior
|
||||
* releases. Use only when strictly required for backward compatibility and with full
|
||||
* understanding that it may accept values that enable attacks against the authorization
|
||||
* server.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author addcontent
|
||||
* @since 7.0.5
|
||||
* @see OAuth2ClientRegistrationAuthenticationContext
|
||||
* @see OAuth2ClientRegistrationAuthenticationToken
|
||||
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2ClientRegistrationAuthenticationValidator
|
||||
implements Consumer<OAuth2ClientRegistrationAuthenticationContext> {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientRegistrationAuthenticationValidator.class);
|
||||
|
||||
/**
|
||||
* The default validator for {@link OAuth2ClientRegistration#getRedirectUris()
|
||||
* redirect_uris}. Rejects URIs that contain a fragment, have no scheme (e.g.
|
||||
* protocol-relative {@code //host/path}), or use an unsafe scheme
|
||||
* ({@code javascript}, {@code data}, {@code vbscript}).
|
||||
*/
|
||||
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUris;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OAuth2ClientRegistration#getRedirectUris()
|
||||
* redirect_uris} that preserves prior behavior (fragment-only check). Use only when
|
||||
* backward compatibility is required; values that enable open redirect and XSS
|
||||
* attacks may be accepted.
|
||||
*/
|
||||
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}.
|
||||
* Rejects URIs that do not use the {@code https} scheme.
|
||||
*/
|
||||
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUri;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}
|
||||
* that preserves prior behavior (no validation). Use only when backward compatibility
|
||||
* is required; values that enable SSRF attacks may be accepted.
|
||||
*/
|
||||
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OAuth2ClientRegistration#getScopes() scope}.
|
||||
* Rejects any request that includes a non-empty scope value. Deployers that need to
|
||||
* accept scopes during Dynamic Client Registration must configure their own validator
|
||||
* (for example, by chaining on top of {@link #SIMPLE_SCOPE_VALIDATOR}).
|
||||
*/
|
||||
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScope;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OAuth2ClientRegistration#getScopes() scope} that
|
||||
* preserves prior behavior (accepts any scope). Use only when backward compatibility
|
||||
* is required; values that enable arbitrary scope injection may be accepted.
|
||||
*/
|
||||
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScopeSimple;
|
||||
|
||||
private final Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||
|
||||
@Override
|
||||
public void accept(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
}
|
||||
|
||||
private static void validateRedirectUris(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||
return;
|
||||
}
|
||||
for (String redirectUri : redirectUris) {
|
||||
URI parsed;
|
||||
try {
|
||||
parsed = new URI(redirectUri);
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER
|
||||
.debug(LogMessage.format("Invalid request: redirect_uri is not parseable ('%s')", redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
return;
|
||||
}
|
||||
if (parsed.getFragment() != null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(
|
||||
LogMessage.format("Invalid request: redirect_uri contains a fragment ('%s')", redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
String scheme = parsed.getScheme();
|
||||
if (scheme == null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format("Invalid request: redirect_uri has no scheme ('%s')", redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
if (isUnsafeScheme(scheme)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(
|
||||
LogMessage.format("Invalid request: redirect_uri uses unsafe scheme ('%s')", redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisSimple(
|
||||
OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||
return;
|
||||
}
|
||||
for (String redirectUri : redirectUris) {
|
||||
try {
|
||||
URI parsed = new URI(redirectUri);
|
||||
if (parsed.getFragment() != null) {
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJwkSetUri(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
URL jwkSetUrl = clientRegistrationAuthentication.getClientRegistration().getJwkSetUrl();
|
||||
if (jwkSetUrl == null) {
|
||||
return;
|
||||
}
|
||||
if (!"https".equalsIgnoreCase(jwkSetUrl.getProtocol())) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format("Invalid request: jwks_uri does not use https ('%s')", jwkSetUrl));
|
||||
}
|
||||
throwInvalidClientRegistration("invalid_client_metadata", OAuth2ClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJwkSetUriSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
// No validation. Preserves prior behavior.
|
||||
}
|
||||
|
||||
private static void validateScope(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> scopes = clientRegistrationAuthentication.getClientRegistration().getScopes();
|
||||
if (!CollectionUtils.isEmpty(scopes)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format(
|
||||
"Invalid request: scope must not be set during Dynamic Client Registration ('%s')", scopes));
|
||||
}
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ClientMetadataClaimNames.SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateScopeSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||
// No validation. Preserves prior behavior.
|
||||
}
|
||||
|
||||
private static boolean isUnsafeScheme(String scheme) {
|
||||
return "javascript".equalsIgnoreCase(scheme) || "data".equalsIgnoreCase(scheme)
|
||||
|| "vbscript".equalsIgnoreCase(scheme);
|
||||
}
|
||||
|
||||
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -34,6 +35,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class OAuth2DeviceCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 8364555864666204030L;
|
||||
|
||||
private final String deviceCode;
|
||||
|
||||
/**
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
@@ -36,6 +37,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class OAuth2RefreshTokenAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 328697547826078993L;
|
||||
|
||||
private final String refreshToken;
|
||||
|
||||
private final Set<String> scopes;
|
||||
|
||||
+6
-1
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -33,7 +35,10 @@ import org.springframework.util.Assert;
|
||||
* @since 7.0
|
||||
* @see OAuth2TokenExchangeCompositeAuthenticationToken
|
||||
*/
|
||||
public final class OAuth2TokenExchangeActor implements ClaimAccessor {
|
||||
public final class OAuth2TokenExchangeActor implements ClaimAccessor, Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -3966261411784615574L;
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -37,6 +38,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 2484741634669297785L;
|
||||
|
||||
private final String requestedTokenType;
|
||||
|
||||
private final String subjectToken;
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -35,6 +36,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class OAuth2TokenExchangeCompositeAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1912280308201180854L;
|
||||
|
||||
private final Authentication subject;
|
||||
|
||||
private final List<OAuth2TokenExchangeActor> actors;
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an
|
||||
* {@link OidcClientRegistrationAuthenticationToken} and additional information and is
|
||||
* used when validating the OpenID Connect 1.0 Client Registration Request parameters.
|
||||
*
|
||||
* @author addcontent
|
||||
* @since 7.0.5
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OidcClientRegistrationAuthenticationToken
|
||||
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OidcClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OidcClientRegistrationAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided
|
||||
* {@link OidcClientRegistrationAuthenticationToken}.
|
||||
* @param authentication the {@link OidcClientRegistrationAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OidcClientRegistrationAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OidcClientRegistrationAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OidcClientRegistrationAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OidcClientRegistrationAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OidcClientRegistrationAuthenticationContext}.
|
||||
* @return the {@link OidcClientRegistrationAuthenticationContext}
|
||||
*/
|
||||
@Override
|
||||
public OidcClientRegistrationAuthenticationContext build() {
|
||||
return new OidcClientRegistrationAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+29
-34
@@ -16,14 +16,12 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -60,7 +58,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -102,6 +99,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
private Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
@@ -121,6 +120,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
|
||||
this.registeredClientConverter = new OidcClientRegistrationRegisteredClientConverter();
|
||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
this.authenticationValidator = new OidcClientRegistrationAuthenticationValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,20 +206,35 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the
|
||||
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
|
||||
* validating specific OpenID Connect 1.0 Client Registration Request parameters
|
||||
* associated in the {@link OidcClientRegistrationAuthenticationToken}. The default
|
||||
* authentication validator is {@link OidcClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw
|
||||
* {@link OAuth2AuthenticationException} if validation fails.
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the
|
||||
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
|
||||
* validating specific OpenID Connect 1.0 Client Registration Request parameters
|
||||
* @since 7.0.5
|
||||
*/
|
||||
public void setAuthenticationValidator(
|
||||
Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private OidcClientRegistrationAuthenticationToken registerClient(
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
if (!isValidRedirectUris(
|
||||
clientRegistrationAuthentication.getClientRegistration().getPostLogoutRedirectUris())) {
|
||||
throwInvalidClientRegistration("invalid_client_metadata",
|
||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
OidcClientRegistrationAuthenticationContext authenticationContext = OidcClientRegistrationAuthenticationContext
|
||||
.with(clientRegistrationAuthentication)
|
||||
.build();
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
|
||||
if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) {
|
||||
throwInvalidClientRegistration("invalid_client_metadata",
|
||||
@@ -351,26 +366,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidRedirectUris(List<String> redirectUris) {
|
||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String redirectUri : redirectUris) {
|
||||
try {
|
||||
URI validRedirectUri = new URI(redirectUri);
|
||||
if (validRedirectUri.getFragment() != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) {
|
||||
String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod();
|
||||
String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user