Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53bc6a7796 | |||
| aed95aa2b1 | |||
| 7b57a4a3bb | |||
| 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
|
||||
|
||||
@@ -18,21 +18,21 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
- 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@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
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@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
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@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.next-version.outputs.version }}
|
||||
version-date: ${{ steps.next-version.outputs.version-date }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
|
||||
with:
|
||||
java-version: '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 }}
|
||||
|
||||
@@ -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
@@ -238,7 +238,9 @@ class HttpSecurityConfiguration {
|
||||
Map<Class<?>, Object> sharedObjects = new HashMap<>();
|
||||
sharedObjects.put(ApplicationContext.class, this.context);
|
||||
sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy);
|
||||
sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context));
|
||||
sharedObjects.put(PathPatternRequestMatcher.Builder.class,
|
||||
this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
|
||||
.getIfUnique(() -> constructRequestMatcherBuilder(this.context)));
|
||||
return sharedObjects;
|
||||
}
|
||||
|
||||
|
||||
+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("\\.");
|
||||
|
||||
+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 {
|
||||
|
||||
+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 {
|
||||
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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();
|
||||
|
||||
+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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"@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.11",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.5
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -12,7 +12,7 @@ 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.7"
|
||||
com-password4j = "1.8.4"
|
||||
|
||||
[libraries]
|
||||
@@ -30,7 +30,7 @@ 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-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.5"
|
||||
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 +51,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.15"
|
||||
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" }
|
||||
@@ -81,8 +81,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
|
||||
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-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.5"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.3"
|
||||
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 +102,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.3.RELEASE'
|
||||
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
|
||||
|
||||
[plugins]
|
||||
|
||||
+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;
|
||||
|
||||
/**
|
||||
|
||||
+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();
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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.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.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A {@code Consumer} providing access to the
|
||||
* {@link OidcClientRegistrationAuthenticationContext} containing an
|
||||
* {@link OidcClientRegistrationAuthenticationToken} and is the default
|
||||
* {@link OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
* authentication validator} used for validating specific OpenID Connect 1.0 Dynamic
|
||||
* Client Registration Request parameters.
|
||||
*
|
||||
* <p>
|
||||
* The default implementation validates {@link OidcClientRegistration#getRedirectUris()
|
||||
* redirect_uris}, {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||
* post_logout_redirect_uris}, {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}, and
|
||||
* {@link OidcClientRegistration#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 OidcClientRegistrationAuthenticationContext
|
||||
* @see OidcClientRegistrationAuthenticationToken
|
||||
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OidcClientRegistrationAuthenticationValidator
|
||||
implements Consumer<OidcClientRegistrationAuthenticationContext> {
|
||||
|
||||
private static final String ERROR_URI = "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError";
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(OidcClientRegistrationAuthenticationValidator.class);
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUris;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||
* post_logout_redirect_uris}. Applies the same rules as
|
||||
* {@link #DEFAULT_REDIRECT_URI_VALIDATOR}.
|
||||
*/
|
||||
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUris;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||
* post_logout_redirect_uris} that preserves prior behavior (fragment-only check). Use
|
||||
* only when backward compatibility is required; values that enable XSS attacks on the
|
||||
* authorization server origin may be accepted.
|
||||
*/
|
||||
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUrisSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}.
|
||||
* Rejects URIs that do not use the {@code https} scheme.
|
||||
*/
|
||||
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUri;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScope;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScopeSimple;
|
||||
|
||||
private final Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||
|
||||
@Override
|
||||
public void accept(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
}
|
||||
|
||||
private static void validateRedirectUris(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
validateRedirectUrisStrict(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validatePostLogoutRedirectUris(
|
||||
OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
|
||||
.getPostLogoutRedirectUris();
|
||||
validateRedirectUrisStrict(postLogoutRedirectUris, "invalid_client_metadata",
|
||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisStrict(List<String> redirectUris, String errorCode, String fieldName) {
|
||||
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: %s is not parseable ('%s')", fieldName, redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
return;
|
||||
}
|
||||
if (parsed.getFragment() != null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format("Invalid request: %s contains a fragment ('%s')", fieldName,
|
||||
redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
String scheme = parsed.getScheme();
|
||||
if (scheme == null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format("Invalid request: %s has no scheme ('%s')", fieldName, redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
if (isUnsafeScheme(scheme)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(
|
||||
LogMessage.format("Invalid request: %s uses unsafe scheme ('%s')", fieldName, redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
validateRedirectUrisFragmentOnly(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validatePostLogoutRedirectUrisSimple(
|
||||
OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
|
||||
.getPostLogoutRedirectUris();
|
||||
validateRedirectUrisFragmentOnly(postLogoutRedirectUris, "invalid_client_metadata",
|
||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisFragmentOnly(List<String> redirectUris, String errorCode,
|
||||
String fieldName) {
|
||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||
return;
|
||||
}
|
||||
for (String redirectUri : redirectUris) {
|
||||
try {
|
||||
URI parsed = new URI(redirectUri);
|
||||
if (parsed.getFragment() != null) {
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJwkSetUri(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken 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", OidcClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJwkSetUriSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
// No validation. Preserves prior behavior.
|
||||
}
|
||||
|
||||
private static void validateScope(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken 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, OidcClientMetadataClaimNames.SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateScopeSimple(OidcClientRegistrationAuthenticationContext 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);
|
||||
}
|
||||
|
||||
}
|
||||
+17
-1
@@ -24,6 +24,9 @@ import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
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.SessionInformation;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
@@ -141,7 +144,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
|
||||
SessionInformation sessionInformation = context.get(SessionInformation.class);
|
||||
if (sessionInformation != null) {
|
||||
claimsBuilder.claim("sid", sessionInformation.getSessionId());
|
||||
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, sessionInformation.getLastRequest());
|
||||
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, getAuthenticationTime(context.getPrincipal()));
|
||||
}
|
||||
}
|
||||
else if (AuthorizationGrantType.REFRESH_TOKEN.equals(context.getAuthorizationGrantType())) {
|
||||
@@ -222,4 +225,17 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
static Date getAuthenticationTime(Authentication authentication) {
|
||||
Instant authenticationTime = null;
|
||||
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
|
||||
if (grantedAuthority instanceof FactorGrantedAuthority factorGrantedAuthority) {
|
||||
if (authenticationTime == null || factorGrantedAuthority.getIssuedAt().isAfter(authenticationTime)) {
|
||||
authenticationTime = factorGrantedAuthority.getIssuedAt();
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.notNull(authenticationTime, "authenticationTime cannot be null");
|
||||
return Date.from(authenticationTime);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -154,6 +154,7 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
+7
-2
@@ -24,6 +24,9 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
@@ -85,6 +88,9 @@ public final class TestOAuth2Authorizations {
|
||||
.additionalParameters(authorizationRequestAdditionalParameters)
|
||||
.state("state")
|
||||
.build();
|
||||
Authentication principal = new TestingAuthenticationToken("principal", null,
|
||||
new SimpleGrantedAuthority("ROLE_A"), new SimpleGrantedAuthority("ROLE_B"),
|
||||
FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY));
|
||||
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.id("id")
|
||||
.principalName("principal")
|
||||
@@ -93,8 +99,7 @@ public final class TestOAuth2Authorizations {
|
||||
.token(authorizationCode)
|
||||
.attribute(OAuth2ParameterNames.STATE, "consent-state")
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
|
||||
.attribute(Principal.class.getName(),
|
||||
new TestingAuthenticationToken("principal", null, "ROLE_A", "ROLE_B"));
|
||||
.attribute(Principal.class.getName(), principal);
|
||||
if (accessToken != null) {
|
||||
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now(),
|
||||
Instant.now().plus(1, ChronoUnit.HOURS));
|
||||
|
||||
+9
@@ -360,6 +360,11 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
@@ -412,6 +417,10 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
|
||||
@Test
|
||||
public void authenticateWhenOpenRegistrationThenReturnClientRegistration() {
|
||||
this.authenticationProvider.setOpenRegistrationAllowed(true);
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
|
||||
// @formatter:off
|
||||
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2ClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* @author addcontent
|
||||
*/
|
||||
public class OAuth2ClientRegistrationAuthenticationValidatorTests {
|
||||
|
||||
private final OAuth2ClientRegistrationAuthenticationValidator validator = new OAuth2ClientRegistrationAuthenticationValidator();
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||
assertRejected(context("//client.example.com/path", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||
assertRejected(context("javascript:alert(document.cookie)", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenDataSchemeThenRejected() {
|
||||
assertRejected(context("data:text/html,<h1>content</h1>", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenVbscriptSchemeThenRejected() {
|
||||
assertRejected(context("vbscript:msgbox(\"content\")", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenFragmentThenRejected() {
|
||||
assertRejected(context("https://client.example.com/cb#fragment", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenCustomSchemeForNativeAppThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("myapp://callback", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenHttpLoopbackThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("http://127.0.0.1:8080", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
|
||||
assertRejected(context("https://client.example.com", "http://169.254.169.254/keys"), "invalid_client_metadata",
|
||||
OAuth2ClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> this.validator.accept(context("https://client.example.com", "https://client.example.com/jwks")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenAbsentThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||
OAuth2ClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.scope("write")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultScopeValidatorWhenEmptyThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRedirectUriValidatorWhenProtocolRelativeThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = context("//client.example.com/path", null);
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null);
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = context("https://client.example.com",
|
||||
"http://169.254.169.254/keys");
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||
OAuth2ClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.scope("write")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
|
||||
Consumer<OAuth2ClientRegistrationAuthenticationContext> composed = OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
|
||||
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||
OAuth2ClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.jwkSetUrl("https://client.example.com/jwks")
|
||||
.scope("openid")
|
||||
.scope("profile")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatNoException().isThrownBy(() -> composed.accept(context));
|
||||
}
|
||||
|
||||
private static OAuth2ClientRegistrationAuthenticationContext context(String redirectUri, String jwkSetUrl) {
|
||||
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder();
|
||||
if (redirectUri != null) {
|
||||
builder.redirectUri(redirectUri);
|
||||
}
|
||||
if (jwkSetUrl != null) {
|
||||
builder.jwkSetUrl(jwkSetUrl);
|
||||
}
|
||||
return OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null, builder.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertRejected(OAuth2ClientRegistrationAuthenticationContext context, String errorCode,
|
||||
String fieldName) {
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(errorCode);
|
||||
assertThat(error.getDescription()).contains(fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -561,6 +561,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
|
||||
this.authenticationProvider
|
||||
.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));
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
@@ -612,6 +617,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenRegistrationAccessTokenNotGeneratedThenThrowOAuth2AuthenticationException() {
|
||||
this.authenticationProvider
|
||||
.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));
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
@@ -653,6 +663,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||
this.authenticationProvider
|
||||
.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));
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* @author addcontent
|
||||
*/
|
||||
public class OidcClientRegistrationAuthenticationValidatorTests {
|
||||
|
||||
private final OidcClientRegistrationAuthenticationValidator validator = new OidcClientRegistrationAuthenticationValidator();
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||
assertRejected(context("//client.example.com/path", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||
assertRejected(context("javascript:alert(document.cookie)", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> this.validator.accept(context("https://client.example.com", null, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPostLogoutRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||
assertRejected(context("https://client.example.com", "javascript:alert(document.cookie)", null),
|
||||
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPostLogoutRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||
assertRejected(context("https://client.example.com", "//client.example.com/post-logout", null),
|
||||
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPostLogoutRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator
|
||||
.accept(context("https://client.example.com", "https://client.example.com/post-logout", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
|
||||
assertRejected(context("https://client.example.com", null, "http://169.254.169.254/keys"),
|
||||
"invalid_client_metadata", OidcClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator
|
||||
.accept(context("https://client.example.com", null, "https://client.example.com/jwks")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
|
||||
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
|
||||
.build();
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null, null);
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simplePostLogoutRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext context = context("https://client.example.com",
|
||||
"javascript:alert(document.cookie)", null);
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR
|
||||
.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext ctx = context("https://client.example.com", null,
|
||||
"http://169.254.169.254/keys");
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
|
||||
.build();
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
|
||||
Consumer<OidcClientRegistrationAuthenticationContext> composed = 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);
|
||||
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||
OidcClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.postLogoutRedirectUri("https://client.example.com/post-logout")
|
||||
.jwkSetUrl("https://client.example.com/jwks")
|
||||
.scope("openid")
|
||||
.scope("profile")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatNoException().isThrownBy(() -> composed.accept(context));
|
||||
}
|
||||
|
||||
private static Authentication principal() {
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password");
|
||||
principal.setAuthenticated(true);
|
||||
return principal;
|
||||
}
|
||||
|
||||
private static OidcClientRegistrationAuthenticationContext context(String redirectUri, String postLogoutRedirectUri,
|
||||
String jwkSetUrl) {
|
||||
OidcClientRegistration.Builder builder = OidcClientRegistration.builder();
|
||||
if (redirectUri != null) {
|
||||
builder.redirectUri(redirectUri);
|
||||
}
|
||||
if (postLogoutRedirectUri != null) {
|
||||
builder.postLogoutRedirectUri(postLogoutRedirectUri);
|
||||
}
|
||||
if (jwkSetUrl != null) {
|
||||
builder.jwkSetUrl(jwkSetUrl);
|
||||
}
|
||||
return OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(), builder.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertRejected(OidcClientRegistrationAuthenticationContext context, String errorCode,
|
||||
String fieldName) {
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(errorCode);
|
||||
assertThat(error.getDescription()).contains(fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -363,7 +363,7 @@ public class JwtGeneratorTests {
|
||||
SessionInformation sessionInformation = tokenContext.get(SessionInformation.class);
|
||||
assertThat(jwtClaimsSet.<String>getClaim("sid")).isEqualTo(sessionInformation.getSessionId());
|
||||
assertThat(jwtClaimsSet.<Date>getClaim(IdTokenClaimNames.AUTH_TIME))
|
||||
.isEqualTo(sessionInformation.getLastRequest());
|
||||
.isEqualTo(JwtGenerator.getAuthenticationTime(tokenContext.getPrincipal()));
|
||||
}
|
||||
else if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.REFRESH_TOKEN)) {
|
||||
OidcIdToken currentIdToken = tokenContext.getAuthorization().getToken(OidcIdToken.class).getToken();
|
||||
|
||||
+5
-1
@@ -300,7 +300,11 @@ public final class OidcAuthorizedClientRefreshedEventListener
|
||||
return;
|
||||
}
|
||||
|
||||
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
|
||||
// The auth_time claim MUST represent the time of the original authentication OR
|
||||
// the most recent time when the end-user reauthenticated when "prompt=login" is
|
||||
// passed in the authentication request
|
||||
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
|
||||
&& !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
|
||||
REFRESH_TOKEN_RESPONSE_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
|
||||
+1
@@ -266,6 +266,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class OAuth2AuthorizationRequestException extends AuthenticationException {
|
||||
|
||||
OAuth2AuthorizationRequestException(Throwable cause) {
|
||||
|
||||
+21
-3
@@ -419,8 +419,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtDoesNotMatchThenThrowsOAuth2AuthenticationException() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.SECONDS);
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtBeforeCurrentAuthenticatedAtThenThrowsOAuth2AuthenticationException() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().minus(5, ChronoUnit.MINUTES);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
|
||||
@@ -440,8 +440,26 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
|
||||
verifyNoInteractions(this.userService, this.applicationEventPublisher);
|
||||
}
|
||||
|
||||
// gh-18839
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesThenOidcUserRefreshedEventPublished() {
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtAfterCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.MINUTES);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
|
||||
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
|
||||
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
|
||||
|
||||
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
|
||||
this.accessTokenResponse, this.authorizedClient);
|
||||
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
|
||||
|
||||
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
|
||||
verifyNoMoreInteractions(this.applicationEventPublisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
|
||||
Instant authTime = this.authentication.getPrincipal().getAttribute(IdTokenClaimNames.AUTH_TIME);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
|
||||
+35
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.oidc.user;
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
@@ -114,4 +115,38 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
||||
return this.userInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DefaultOidcUser that = (DefaultOidcUser) obj;
|
||||
if (!this.getName().equals(that.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (!this.getAuthorities().equals(that.getAuthorities())) {
|
||||
return false;
|
||||
}
|
||||
if (this.getIdToken().getIssuer() == null || that.getIdToken().getIssuer() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(this.getIdToken().getIssuer().toExternalForm(),
|
||||
that.getIdToken().getIssuer().toExternalForm())
|
||||
&& Objects.equals(this.getIdToken().getSubject(), that.getIdToken().getSubject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.getName().hashCode();
|
||||
result = 31 * result + this.getAuthorities().hashCode();
|
||||
result = 31 * result + ((this.getIdToken().getIssuer() != null)
|
||||
? this.getIdToken().getIssuer().toExternalForm().hashCode() : 0);
|
||||
result = 31 * result
|
||||
+ ((this.getIdToken().getSubject() != null) ? this.getIdToken().getSubject().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TestOidcAuthorizationRequest extends OAuth2AuthorizationRequest {
|
||||
|
||||
private final String nonce;
|
||||
|
||||
+28
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.core.oidc.user;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -147,4 +148,31 @@ public class DefaultOidcUserTests {
|
||||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
// gh-18622
|
||||
@Test
|
||||
public void equalsWhenOidcUserPrincipalSameThenTrue() {
|
||||
String issuer = "https://example.com";
|
||||
String subject = "subject-1";
|
||||
|
||||
// @formatter:off
|
||||
OidcIdToken idToken1 = OidcIdToken.withTokenValue("id-token-value-1")
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
|
||||
.build();
|
||||
|
||||
OidcIdToken idToken2 = OidcIdToken.withTokenValue("id-token-value-2")
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
DefaultOidcUser user1 = new DefaultOidcUser(AUTHORITIES, idToken1, USER_INFO);
|
||||
DefaultOidcUser user2 = new DefaultOidcUser(AUTHORITIES, idToken2, USER_INFO);
|
||||
assertThat(user1).isEqualTo(user2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -185,6 +185,7 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPP
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class JtiCache extends LinkedHashMap<String, Long> {
|
||||
|
||||
private static final int MAX_SIZE = 1000;
|
||||
|
||||
+13
-2
@@ -232,7 +232,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
.getConfigurationForIssuerLocation(issuer, rest);
|
||||
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
|
||||
return configuration.get("jwks_uri").toString();
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,6 +302,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
|
||||
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (rest) -> jwkSetUri;
|
||||
@@ -441,6 +444,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
Assert.notNull(validator, "validator cannot be null");
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
|
||||
if (this.signatureAlgorithms.isEmpty()) {
|
||||
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
|
||||
@@ -479,7 +488,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
* @return the configured {@link NimbusJwtDecoder}
|
||||
*/
|
||||
public NimbusJwtDecoder build() {
|
||||
return new NimbusJwtDecoder(processor());
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
|
||||
|
||||
+12
-2
@@ -241,7 +241,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
}
|
||||
return Mono.just(configuration.get("jwks_uri").toString());
|
||||
}),
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,6 +333,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
|
||||
private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
|
||||
@@ -456,6 +459,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(
|
||||
BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) {
|
||||
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
|
||||
@@ -468,7 +476,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
* @return the configured {@link NimbusReactiveJwtDecoder}
|
||||
*/
|
||||
public NimbusReactiveJwtDecoder build() {
|
||||
return new NimbusReactiveJwtDecoder(processor());
|
||||
NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
|
||||
|
||||
+16
-1
@@ -332,7 +332,10 @@ public class NimbusJwtDecoderTests {
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
|
||||
.restOperations(restOperations)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
@@ -350,6 +353,18 @@ public class NimbusJwtDecoderTests {
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class)))
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
|
||||
// @formatter:off
|
||||
|
||||
+22
-2
@@ -617,11 +617,31 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
WebClient real = WebClient.builder().build();
|
||||
WebClient.RequestHeadersUriSpec spec = spy(real.get());
|
||||
WebClient webClient = spy(WebClient.class);
|
||||
given(webClient.get()).willReturn(spec);
|
||||
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
|
||||
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
assertThatExceptionOfType(JwtValidationException.class)
|
||||
.isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -30,6 +30,23 @@ import org.springframework.util.Assert;
|
||||
* A {@link Jwt} to {@link GrantedAuthority} {@link Converter} that is a composite of
|
||||
* converters.
|
||||
*
|
||||
* <p>
|
||||
* This is handy when needing to read authorities from multiple locations in a JWT; each
|
||||
* underlying converter is called in series and the results are aggregated into a single
|
||||
* collection of authorities.
|
||||
*
|
||||
* <p>
|
||||
* For example, you might have a claim called "scope" and another called "roles". With
|
||||
* {@link DelegatingJwtGrantedAuthoritiesConverter}, you can do:
|
||||
*
|
||||
* <code>
|
||||
* JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
|
||||
* JwtGrantedAuthoritiesConverter roles = new JwtGrantedAUthoritiesConverter();
|
||||
* roles.setAuthoritiesClaimName("roles");
|
||||
* roles.setAuthorityPrefix("ROLE_");
|
||||
* return new DelegatingJwtGrantedAuthoritiesConverter(scopes, roles);
|
||||
* </code>
|
||||
*
|
||||
* @author Laszlo Stahorszki
|
||||
* @author Josh Cummings
|
||||
* @since 5.5
|
||||
|
||||
-3
@@ -36,9 +36,6 @@ import org.springframework.util.Assert;
|
||||
* Uses an expression for extracting the token claim value to use for mapping
|
||||
* {@link GrantedAuthority authorities}.
|
||||
*
|
||||
* Note this can be used in combination with a
|
||||
* {@link DelegatingJwtGrantedAuthoritiesConverter}.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @since 6.4
|
||||
*/
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
@@ -33,6 +34,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class Saml2AuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5225098478444036532L;
|
||||
|
||||
private final RelyingPartyRegistration relyingPartyRegistration;
|
||||
|
||||
private final String saml2Response;
|
||||
|
||||
@@ -258,6 +258,11 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||
|
||||
/**
|
||||
* Convenience method, mainly for testing.
|
||||
* <p>
|
||||
* Attempt to find the matching filter chain based on the given {@code url}. Note that
|
||||
* the URI is often not enough information and this method should be used with
|
||||
* caution. Instead, consider using Spring Security's testing support that mocks a
|
||||
* full HTTP request.
|
||||
* @param url the URL
|
||||
* @return matching filter list
|
||||
*/
|
||||
|
||||
+7
-2
@@ -113,9 +113,14 @@ public abstract class AbstractAuthenticationTargetUrlRequestHandler {
|
||||
trace("Using url %s from request parameter %s", targetUrlParameterValue, this.targetUrlParameter);
|
||||
return targetUrlParameterValue;
|
||||
}
|
||||
|
||||
String refererHeader = request.getHeader("Referer");
|
||||
if (!StringUtils.hasText(refererHeader)) {
|
||||
return this.defaultTargetUrl;
|
||||
}
|
||||
if (this.useReferer) {
|
||||
trace("Using url %s from Referer header", request.getHeader("Referer"));
|
||||
return request.getHeader("Referer");
|
||||
trace("Using url %s from Referer header", refererHeader);
|
||||
return refererHeader;
|
||||
}
|
||||
return this.defaultTargetUrl;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user