1
0
mirror of synced 2026-08-05 17:57:15 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Josh Cummings 0d5b0c4627 Use java-version 2026-02-11 13:07:06 -07:00
Josh Cummings 45ce971d6c Use releases/v1 2026-02-10 11:12:27 -07:00
150 changed files with 559 additions and 5463 deletions
+19 -10
View File
@@ -1,16 +1,25 @@
name: Merge Dependabot PR
name: Dependabot Auto-Merge
on:
pull_request:
branches:
- main
- '*.x'
- 'main'
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions:
contents: write
pull-requests: write
jobs:
merge-dependabot-pr:
permissions: write-all
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v7
with:
mergeArguments: --auto --rebase
dependabot:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7
with:
github-token: ${{ github.token }}
- name: Enable auto-merge for Dependabot PRs
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ github.token }}
+2 -2
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
snapshot-test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
strategy:
matrix:
include:
@@ -31,6 +31,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@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -17,7 +17,7 @@ permissions:
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
@@ -30,7 +30,7 @@ jobs:
deploy-artifacts:
name: Deploy Artifacts
needs: [ build]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@releases/v1
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
default-publish-milestones-central: true
@@ -39,7 +39,7 @@ jobs:
deploy-schema:
name: Deploy Schema
needs: [ build ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@releases/v1
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
java-version: 25
@@ -47,7 +47,7 @@ jobs:
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@releases/v1
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
@@ -64,6 +64,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@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+1 -1
View File
@@ -16,7 +16,7 @@ permissions:
jobs:
perform-release:
name: Perform Release
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
with:
should-perform-release: true
project-version: ${{ inputs.version }}
@@ -9,7 +9,6 @@ permissions:
jobs:
upgrade_wrapper:
name: Execution
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- name: Set up Git configuration
@@ -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@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+1 -1
View File
@@ -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@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -12,9 +12,8 @@ permissions:
jobs:
update-antora-ui-spring:
name: Update on Supported Branches
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
name: Update on Supported Branches
strategy:
matrix:
branch: [ '6.5.x', '7.0.x', 'main' ]
@@ -26,9 +25,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
antora-file-path: 'docs/antora-playbook.yml'
update-antora-ui-spring-docs-build:
name: Update on docs-build
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
name: Update on docs-build
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
name: Update
@@ -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@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
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@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+2 -2
View File
@@ -31,7 +31,7 @@ If you have a question, check Stack Overflow using
https://stackoverflow.com/questions/tagged/spring-security+or+spring-ldap+or+spring-authorization-server+or+spring-session?tab=Newest[this list of tags].
Find an existing discussion, or start a new one if necessary.
If you believe there is an issue, search through https://github.com/spring-projects/spring-security/issues[existing issues] trying a few different ways to find discussions, past or current, that are related to the issue.
If you believe there is an issue, search through https://github.com/spring-projects/spring-security/issues[existing issues] trying a few different ways to find discussions, past or current, that are related to the issue.
Reading those discussions helps you to learn about the issue, and helps us to make a decision.
[[find-an-issue]]
@@ -94,7 +94,7 @@ Don't worry if you don't get them all correct the first time, we will help you.
1. [[sign-cla]] All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring].
2. [[create-an-issue-list]] Must you https://github.com/spring-projects/spring-security/issues/new/choose[create an issue] first? No, but it is recommended for features and larger bug fixes. It's easier to discuss with the team first to determine the right fix or enhancement.
2. [[create-an-issue-list]] Must you https://github.com/spring-projects/spring-security/issues/new/choose[create an issue] first? No, but it is recommended for features and larger bug fixes. It's easier discuss with the team first to determine the right fix or enhancement.
For typos and straightforward bug fixes, starting with a pull request is encouraged.
Please include a description for context and motivation.
Note that the team may close your pull request if it's not a fit for the project.
@@ -31,7 +31,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.method.AbstractFallbackMethodSecurityMetadataSource;
import org.springframework.util.StringUtils;
/**
* Sources method security metadata from major JSR 250 security annotations.
@@ -109,7 +108,7 @@ public class Jsr250MethodSecurityMetadataSource extends AbstractFallbackMethodSe
if (role == null) {
return role;
}
if (!StringUtils.hasLength(this.defaultRolePrefix)) {
if (this.defaultRolePrefix == null || this.defaultRolePrefix.length() == 0) {
return role;
}
if (role.startsWith(this.defaultRolePrefix)) {
@@ -1,79 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.acls.aot.hint;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.acls.domain.AclImpl;
import org.springframework.security.acls.domain.AuditLogger;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.AccessControlEntry;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AuditableAccessControlEntry;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Sid;
/**
* {@link RuntimeHintsRegistrar} for ACL (Access Control List) classes.
*
* @author Josh Long
*/
class AclRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
registerAclDomainHints(hints);
registerJdbcSchemaHints(hints);
}
private void registerAclDomainHints(RuntimeHints hints) {
// Register core ACL domain types
Stream
.of(Acl.class, AccessControlEntry.class, AuditableAccessControlEntry.class, ObjectIdentity.class, Sid.class,
AclImpl.class, AccessControlEntry.class, AuditLogger.class, ObjectIdentityImpl.class,
PrincipalSid.class, GrantedAuthoritySid.class, BasePermission.class)
.forEach((c) -> hints.reflection()
.registerType(TypeReference.of(c),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS)));
}
private void registerJdbcSchemaHints(RuntimeHints hints) {
String[] sqlFiles = new String[] { "createAclSchema.sql", "createAclSchemaMySQL.sql",
"createAclSchemaOracle.sql", "createAclSchemaPostgres.sql", "createAclSchemaSqlServer.sql",
"createAclSchemaWithAclClassIdType.sql", "select.sql" };
for (String sqlFile : sqlFiles) {
Resource sqlResource = new ClassPathResource(sqlFile);
if (sqlResource.exists()) {
hints.resources().registerResource(sqlResource);
}
}
}
}
@@ -1,23 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* AOT and native image hint support for ACLs.
*/
@NullMarked
package org.springframework.security.acls.aot.hint;
import org.jspecify.annotations.NullMarked;
@@ -1,2 +0,0 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.security.acls.aot.hint.AclRuntimeHints
@@ -34,7 +34,6 @@ import org.springframework.util.Assert;
* and using the current URL minus the artifact and the corresponding value.
*
* @author Rob Winch
* @author Ngoc Nhan
*/
final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
implements ServiceAuthenticationDetails {
@@ -75,9 +74,10 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
if (this == obj) {
return true;
}
if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails that)) {
if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails)) {
return false;
}
ServiceAuthenticationDetails that = (ServiceAuthenticationDetails) obj;
return this.serviceUrl.equals(that.getServiceUrl());
}
@@ -101,11 +101,7 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
/**
* If present, removes the artifactParameterName and the corresponding value from the
* query String.
* @param request the current {@link HttpServletRequest} to obtain the
* {@link #getServiceUrl()} from.
* @param artifactPattern the {@link Pattern} that will be used to clean up the query
* string from containing the artifact name and value. This can be created using
* {@link #createArtifactPattern(String)}.
* @param request
* @return the query String minus the artifactParameterName and the corresponding
* value.
*/
@@ -115,7 +111,7 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
return null;
}
String result = artifactPattern.matcher(query).replaceFirst("");
if (result.isEmpty()) {
if (result.length() == 0) {
return null;
}
// strip off the trailing & only if the artifact was the first query param
@@ -126,9 +122,8 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
* Creates a {@link Pattern} that can be passed into the constructor. This allows the
* {@link Pattern} to be reused for every instance of
* {@link DefaultServiceAuthenticationDetails}.
* @param artifactParameterName the artifactParameterName that is removed from the
* current URL. The result becomes the service url. Cannot be null or an empty String.
* @return a {@link Pattern}
* @param artifactParameterName
* @return
*/
static Pattern createArtifactPattern(String artifactParameterName) {
Assert.hasLength(artifactParameterName, "artifactParameterName is expected to have a length");
@@ -36,7 +36,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.log.LogMessage;
import org.springframework.lang.Contract;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
@@ -303,7 +302,6 @@ public class AuthenticationConfiguration {
}
@Override
@Contract("!null -> !null; null -> null")
public String encode(CharSequence rawPassword) {
return getPasswordEncoder().encode(rawPassword);
}
@@ -30,7 +30,6 @@ import org.springframework.context.annotation.Scope;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Contract;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
@@ -294,7 +293,6 @@ class HttpSecurityConfiguration {
}
@Override
@Contract("!null -> !null; null -> null")
public String encode(CharSequence rawPassword) {
return getPasswordEncoder().encode(rawPassword);
}
@@ -124,10 +124,6 @@ class AuthorizationFilterParser implements BeanDefinitionParser {
List<Element> interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_URL);
for (Element interceptMessage : interceptMessages) {
String accessExpression = interceptMessage.getAttribute(ATT_ACCESS);
if (!StringUtils.hasText(accessExpression)) {
parserContext.getReaderContext().error("access attribute cannot be empty or null", interceptMessage);
continue;
}
BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
.rootBeanDefinition(WebExpressionAuthorizationManager.class);
authorizationManager.addPropertyReference("expressionHandler", expressionHandlerRef);
@@ -142,11 +142,10 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
ManagedMap<BeanMetadataElement, BeanDefinition> filterInvocationDefinitionMap = new ManagedMap<>();
for (Element urlElt : urlElts) {
String access = urlElt.getAttribute(ATT_ACCESS);
String path = urlElt.getAttribute(ATT_PATTERN);
if (!StringUtils.hasText(access)) {
parserContext.getReaderContext().error("access attribute cannot be empty or null", urlElt);
continue;
}
String path = urlElt.getAttribute(ATT_PATTERN);
String matcherRef = urlElt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUEST_MATCHER_REF);
boolean hasMatcherRef = StringUtils.hasText(matcherRef);
if (!hasMatcherRef && !StringUtils.hasText(path)) {
@@ -20,7 +20,6 @@ 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;
@@ -246,15 +245,12 @@ import org.springframework.security.web.savedrequest.SimpleSavedRequest;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
import org.springframework.security.web.session.HttpSessionCreatedEvent;
import org.springframework.security.web.session.HttpSessionIdChangedEvent;
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
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.AuthenticatorSelectionCriteria;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInput;
@@ -262,17 +258,12 @@ import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExte
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialParameters;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.ResidentKeyRequirement;
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
import org.springframework.security.web.webauthn.api.TestBytes;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentials;
@@ -280,7 +271,6 @@ import org.springframework.security.web.webauthn.api.UserVerificationRequirement
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationRequestToken;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.security.web.webauthn.management.TestPublicKeyCredentialRpEntities;
import org.springframework.util.ReflectionUtils;
final class SerializationSamples {
@@ -889,36 +879,6 @@ final class SerializationSamples {
generatorByClassName.put(CredentialPropertiesOutput.ExtensionOutput.class,
(r) -> new CredentialPropertiesOutput(true).getOutput());
AttestationConveyancePreference attestationConveyancePreference = AttestationConveyancePreference.DIRECT;
ResidentKeyRequirement residentKeyRequirement = ResidentKeyRequirement.REQUIRED;
AuthenticatorSelectionCriteria authenticatorSelectionCriteria = AuthenticatorSelectionCriteria.builder()
.authenticatorAttachment(AuthenticatorAttachment.PLATFORM)
.residentKey(residentKeyRequirement)
.userVerification(UserVerificationRequirement.REQUIRED)
.build();
PublicKeyCredentialParameters publicKeyCredentialParameters = PublicKeyCredentialParameters.RS256;
PublicKeyCredentialRpEntity publicKeyCredentialRpEntity = TestPublicKeyCredentialRpEntities.createRpEntity()
.build();
generatorByClassName.put(AttestationConveyancePreference.class, (r) -> attestationConveyancePreference);
generatorByClassName.put(ResidentKeyRequirement.class, (r) -> residentKeyRequirement);
generatorByClassName.put(AuthenticatorSelectionCriteria.class, (r) -> authenticatorSelectionCriteria);
generatorByClassName.put(COSEAlgorithmIdentifier.class, ((r) -> COSEAlgorithmIdentifier.RS256));
generatorByClassName.put(PublicKeyCredentialParameters.class, (r) -> publicKeyCredentialParameters);
generatorByClassName.put(PublicKeyCredentialRpEntity.class, (r) -> publicKeyCredentialRpEntity);
generatorByClassName.put(PublicKeyCredentialCreationOptions.class,
(o) -> TestPublicKeyCredentialCreationOptions.createPublicKeyCredentialCreationOptions()
.extensions(inputs)
.attestation(attestationConveyancePreference)
.authenticatorSelection(authenticatorSelectionCriteria)
.challenge(TestBytes.get())
.excludeCredentials(List.of(descriptor))
.rp(publicKeyCredentialRpEntity)
.pubKeyCredParams(publicKeyCredentialParameters)
.timeout(Duration.ofMinutes(5))
.user(TestPublicKeyCredentialUserEntities.userEntity().id(TestBytes.get()).build())
.build());
// One-Time Token
DefaultOneTimeToken oneTimeToken = new DefaultOneTimeToken(UUID.randomUUID().toString(), "user",
Instant.now().plusSeconds(300));
@@ -222,8 +222,7 @@ public class WebSecurityConfigurationTests {
// SEC-2773
@Test
public void getMethodDelegatingApplicationListenerWhenWebSecurityConfigurationThenIsStatic() {
Method method = ClassUtils.getMethod(WebSecurityConfiguration.class, "delegatingApplicationListener",
(Class<?>[]) null);
Method method = ClassUtils.getMethod(WebSecurityConfiguration.class, "delegatingApplicationListener", null);
assertThat(Modifier.isStatic(method.getModifiers())).isTrue();
}
@@ -337,54 +337,6 @@ public class InterceptUrlConfigTests {
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
}
/**
* gh-18503
*/
@Test
public void configWhenInterceptUrlMissingAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("MissingAccess")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
/**
* gh-18503
*/
@Test
public void configWhenInterceptUrlEmptyAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("EmptyAccess")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
/**
* gh-18503
*/
@Test
public void configWhenInterceptUrlValidAccessThenLoads() {
assertThatNoException().isThrownBy(() -> this.spring.configLocations(this.xml("ValidAccess")).autowire());
}
/**
* gh-18503
*/
@Test
public void configWhenUseAuthorizationManagerFalseAndMissingAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("MissingAccessLegacy")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
/**
* gh-18503
*/
@Test
public void configWhenUseAuthorizationManagerFalseAndEmptyAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("EmptyAccessLegacy")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
private static RequestPostProcessor adminCredentials() {
return httpBasic("admin", "password");
}
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<intercept-url pattern="/admin/**" access="" />
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 use-authorization-manager="false">
<intercept-url pattern="/admin/**" access=""/>
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<intercept-url pattern="/admin/**"/>
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 use-authorization-manager="false">
<intercept-url pattern="/admin/**"/>
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')" />
<http-basic />
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -38,7 +38,6 @@ import org.springframework.util.function.SingletonSupplier;
* @author Luke Taylor
* @author Evgeniy Cheban
* @author Steve Riesenberg
* @author Ngoc Nhan
* @since 3.0
*/
public abstract class SecurityExpressionRoot<T extends @Nullable Object> implements SecurityExpressionOperations {
@@ -209,8 +208,7 @@ public abstract class SecurityExpressionRoot<T extends @Nullable Object> impleme
/**
* Convenience method to access {@link Authentication#getPrincipal()} from
* {@link #getAuthentication()}
* @return the {@code Principal} being authenticated or the authenticated principal
* after authentication.
* @return
*/
public @Nullable Object getPrincipal() {
return getAuthentication().getPrincipal();
@@ -35,8 +35,6 @@ import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.authentication.event.AuthenticationFailureCredentialsExpiredEvent;
import org.springframework.security.authentication.event.AuthenticationFailureDisabledEvent;
@@ -45,7 +43,6 @@ import org.springframework.security.authentication.event.AuthenticationFailureLo
import org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent;
import org.springframework.security.authentication.event.AuthenticationFailureProxyUntrustedEvent;
import org.springframework.security.authentication.event.AuthenticationFailureServiceExceptionEvent;
import org.springframework.security.authentication.ott.OneTimeTokenAuthentication;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
@@ -63,7 +60,6 @@ class CoreSecurityRuntimeHints implements RuntimeHintsRegistrar {
registerExceptionEventsHints(hints);
registerExpressionEvaluationHints(hints);
registerMethodSecurityHints(hints);
registerAdditionalAuthenticationTypes(hints);
hints.resources().registerResourceBundle("org.springframework.security.messages");
registerDefaultJdbcSchemaFileHint(hints);
registerSecurityContextHints(hints);
@@ -118,17 +114,4 @@ class CoreSecurityRuntimeHints implements RuntimeHintsRegistrar {
(builder) -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
}
private void registerAdditionalAuthenticationTypes(RuntimeHints hints) {
// RememberMeAuthenticationToken can be stored in the HTTP session and
// deserialized via Jackson (RememberMeAuthenticationTokenMixin exists for both
// Jackson 2 and 3), so it needs reflection hints in all native image scenarios.
Stream
.of(RememberMeAuthenticationToken.class, OneTimeTokenAuthentication.class,
UsernamePasswordAuthenticationToken.class)
.map(TypeReference::of)
.forEach((it) -> hints.reflection()
.registerType(it, (builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS)));
}
}
@@ -69,11 +69,7 @@ public final class AuthoritiesAuthorizationManager implements AuthorizationManag
private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
String authority = grantedAuthority.getAuthority();
if (authority == null) {
continue;
}
if (authorities.contains(authority)) {
if (authorities.contains(grantedAuthority.getAuthority())) {
return true;
}
}
@@ -87,17 +87,9 @@ public final class RsaKeyConverters {
"Key is not in PEM-encoded PKCS#8 format, please check that the header begins with "
+ PKCS8_PEM_HEADER);
StringBuilder base64Encoded = new StringBuilder();
if (lines.size() == 1) {
base64Encoded.append(lines.get(0)
.replace(PKCS8_PEM_HEADER, "")
.replace(PKCS8_PEM_FOOTER, "")
.replaceAll("\\s+", ""));
}
else {
for (String line : lines) {
if (RsaKeyConverters.isNotPkcs8Wrapper(line)) {
base64Encoded.append(line);
}
for (String line : lines) {
if (RsaKeyConverters.isNotPkcs8Wrapper(line)) {
base64Encoded.append(line);
}
}
byte[] pkcs8 = Base64.getDecoder().decode(base64Encoded.toString());
@@ -173,15 +165,9 @@ public final class RsaKeyConverters {
@Override
public @NonNull RSAPublicKey convert(List<String> lines) {
StringBuilder base64Encoded = new StringBuilder();
if (lines.size() == 1) {
base64Encoded.append(
lines.get(0).replace(X509_PEM_HEADER, "").replace(X509_PEM_FOOTER, "").replaceAll("\\s+", ""));
}
else {
for (String line : lines) {
if (isNotX509PemWrapper(line)) {
base64Encoded.append(line);
}
for (String line : lines) {
if (isNotX509PemWrapper(line)) {
base64Encoded.append(line);
}
}
byte[] x509 = Base64.getDecoder().decode(base64Encoded.toString());
@@ -210,17 +196,9 @@ public final class RsaKeyConverters {
@Override
public @NonNull RSAPublicKey convert(List<String> lines) {
StringBuilder base64Encoded = new StringBuilder();
if (lines.size() == 1) {
base64Encoded.append(lines.get(0)
.replace(X509_CERT_HEADER, "")
.replace(X509_CERT_FOOTER, "")
.replaceAll("\\s+", ""));
}
else {
for (String line : lines) {
if (isNotX509CertificateWrapper(line)) {
base64Encoded.append(line);
}
for (String line : lines) {
if (isNotX509CertificateWrapper(line)) {
base64Encoded.append(line);
}
}
byte[] x509 = Base64.getDecoder().decode(base64Encoded.toString());
@@ -88,6 +88,7 @@ public final class FactorGrantedAuthority implements GrantedAuthority {
private final Instant issuedAt;
@SuppressWarnings("NullAway")
private FactorGrantedAuthority(String authority, Instant issuedAt) {
Assert.notNull(authority, "authority cannot be null");
Assert.notNull(issuedAt, "issuedAt cannot be null");
@@ -31,7 +31,6 @@ class OneTimeTokenAuthenticationTokenTests {
// gh-18095
@Test
@SuppressWarnings("removal")
void shouldBeAbleToDeserializeFromJsonWithDefaultTypingActivated() throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
@@ -59,7 +59,6 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
private static final String TOKEN = "token";
@Test
@SuppressWarnings("removal")
public void constructorWhenOneTimeTokenServiceNullThenIllegalArgumentException() {
ReactiveUserDetailsService userDetailsService = mock(ReactiveUserDetailsService.class);
// @formatter:off
@@ -69,7 +68,6 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
public void constructorWhenUserDetailsServiceNullThenIllegalArgumentException() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
// @formatter:off
@@ -79,7 +77,6 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
void authenticateWhenOneTimeTokenAuthenticationTokenIsPresentThenSuccess() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
given(oneTimeTokenService.consume(ArgumentMatchers.any(OneTimeTokenAuthenticationToken.class)))
@@ -106,7 +103,6 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
void authenticateWhenInvalidOneTimeTokenAuthenticationTokenIsPresentThenFail() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
given(oneTimeTokenService.consume(ArgumentMatchers.any(OneTimeTokenAuthenticationToken.class)))
@@ -124,7 +120,6 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
void authenticateWhenIncorrectTypeOfAuthenticationIsPresentThenFail() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
given(oneTimeTokenService.consume(ArgumentMatchers.any(OneTimeTokenAuthenticationToken.class)))
@@ -17,9 +17,7 @@
package org.springframework.security.authorization;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
@@ -32,13 +30,11 @@ import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
/**
* Tests for {@link AuthoritiesAuthorizationManager}.
*
* @author Evgeniy Cheban
* @author Khyojae
*/
class AuthoritiesAuthorizationManagerTests {
@@ -87,20 +83,4 @@ class AuthoritiesAuthorizationManagerTests {
assertThat(manager.authorize(authentication, Collections.singleton("ROLE_USER")).isGranted()).isTrue();
}
@Test
// gh-18543
void authorizeWhenAuthorityIsNullThenDoesNotThrowNullPointerException() {
AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager();
Authentication authentication = new TestingAuthenticationToken("user", "password",
Collections.singletonList(() -> null));
Collection<String> authoritiesContainsThrowsNPE = Set.of("ROLE_USER");
// must be Collection that throws NPE when .contains(null) is invoked
// to replicate the issue in gh-18543
assertThatNullPointerException().isThrownBy(() -> authoritiesContainsThrowsNPE.contains(null));
assertThat(manager.authorize(() -> authentication, authoritiesContainsThrowsNPE).isGranted()).isFalse();
}
}
@@ -61,7 +61,7 @@ public class AuthorityAuthorizationManagerTests {
@Test
public void hasAnyRoleWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole((String[]) null))
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(null))
.withMessage("roles cannot be empty");
}
@@ -97,8 +97,7 @@ public class AuthorityAuthorizationManagerTests {
@Test
public void hasAnyAuthorityWhenNullThenException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority((String[]) null))
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null))
.withMessage("authorities cannot be empty");
}
@@ -120,13 +120,6 @@ public class RsaKeyConvertersTests {
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(2048);
}
@Test
public void pkcs8WhenConvertingSingleLinePkcs8PrivateKeyThenOk() {
RSAPrivateKey key = this.pkcs8.convert(toInputStream(PKCS8_PRIVATE_KEY.replace("\n", "")));
Assertions.assertThat(key).isInstanceOf(RSAPrivateCrtKey.class);
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(2048);
}
@Test
public void pkcs8WhenConvertingPkcs1PrivateKeyThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.pkcs8.convert(toInputStream(PKCS1_PRIVATE_KEY)));
@@ -138,24 +131,12 @@ public class RsaKeyConvertersTests {
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingSingleLineX509PublicKeyThenOk() {
RSAPublicKey key = this.x509.convert(toInputStream(X509_PUBLIC_KEY.replace("\n", "")));
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingX509CertificateThenOk() {
RSAPublicKey key = this.x509.convert(toInputStream(X509_CERTIFICATE));
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingX509SingleLineCertificateThenOk() {
RSAPublicKey key = this.x509.convert(toInputStream(X509_CERTIFICATE.replace("\n", "")));
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingDerEncodedX509PublicKeyThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.x509.convert(toInputStream(MALFORMED_X509_KEY)));
@@ -73,8 +73,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Map::class.java)
@Suppress("UNCHECKED_CAST")
val result = filtered as Map<String, String>
val result = (filtered as Map<String, String>)
assertThat(result).hasSize(1)
assertThat(result).containsKey("key2")
assertThat(result).containsValue("value2")
@@ -96,8 +95,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Map::class.java)
@Suppress("UNCHECKED_CAST")
val result = filtered as Map<String, String>
val result = (filtered as Map<String, String>)
assertThat(result).hasSize(0)
}
@@ -121,8 +119,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Collection::class.java)
@Suppress("UNCHECKED_CAST")
val result = filtered as Collection<String>
val result = (filtered as Collection<String>)
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@@ -143,8 +140,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Collection::class.java)
@Suppress("UNCHECKED_CAST")
val result = filtered as Collection<String>
val result = (filtered as Collection<String>)
assertThat(result).hasSize(0)
}
@@ -168,8 +164,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Array<String>::class.java)
@Suppress("UNCHECKED_CAST")
val result = filtered as Array<String>
val result = (filtered as Array<String>)
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@@ -190,8 +185,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Array<String>::class.java)
@Suppress("UNCHECKED_CAST")
val result = filtered as Array<String>
val result = (filtered as Array<String>)
assertThat(result).hasSize(0)
}
@@ -215,7 +209,6 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Stream::class.java)
@Suppress("UNCHECKED_CAST")
val result = (filtered as Stream<String>).toList()
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
@@ -237,7 +230,6 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Stream::class.java)
@Suppress("UNCHECKED_CAST")
val result = (filtered as Stream<String>).toList()
assertThat(result).hasSize(0)
}
@@ -18,23 +18,9 @@ package org.springframework.security.crypto.password;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.StringUtils;
/**
* An abstract {@link PasswordEncoder} that implementers can use for expecting the
* password to be non-{@code null}. Each common password API method is accompanied with an
* abstract method with a {@code NonNull} prefix. By implementing this, the concrete class
* is specifying what to do with the password when it is non-{@code null}, allowing this
* class to handle the {@code null} case.
*
* @author Rob Winch
* @since 7.0
*/
public abstract class AbstractValidatingPasswordEncoder implements PasswordEncoder {
@Override
@Contract("!null -> !null; null -> null")
public final @Nullable String encode(@Nullable CharSequence rawPassword) {
if (rawPassword == null) {
return null;
@@ -46,7 +32,8 @@ public abstract class AbstractValidatingPasswordEncoder implements PasswordEncod
@Override
public final boolean matches(@Nullable CharSequence rawPassword, @Nullable String encodedPassword) {
if (!StringUtils.hasLength(rawPassword) || !StringUtils.hasLength(encodedPassword)) {
if (rawPassword == null || rawPassword.length() == 0 || encodedPassword == null
|| encodedPassword.length() == 0) {
return false;
}
return matchesNonNull(rawPassword.toString(), encodedPassword);
@@ -56,7 +43,7 @@ public abstract class AbstractValidatingPasswordEncoder implements PasswordEncod
@Override
public final boolean upgradeEncoding(@Nullable String encodedPassword) {
if (!StringUtils.hasLength(encodedPassword)) {
if (encodedPassword == null || encodedPassword.length() == 0) {
return false;
}
return upgradeEncodingNonNull(encodedPassword);
@@ -38,7 +38,7 @@ public interface PasswordEncoder {
* @return A non-null encoded password, unless the rawPassword was null in which case
* the result must be null.
*/
@Contract("!null -> !null; null -> null")
@Contract("null -> null; !null -> !null")
@Nullable String encode(@Nullable CharSequence rawPassword);
/**
@@ -337,9 +337,8 @@ Protecting multipart requests (file uploads) from CSRF attacks causes a https://
To prevent a CSRF attack from occurring, the body of the HTTP request must be read to obtain the actual CSRF token.
However, reading the body means that the file is uploaded, which means an external site can upload a file.
There are three options to using CSRF protection with multipart/form-data:
There are two options to using CSRF protection with multipart/form-data:
* <<csrf-considerations-multipart-header,Include CSRF Token in an HTTP Request Header>>
* <<csrf-considerations-multipart-body,Place CSRF Token in the Body>>
* <<csrf-considerations-multipart-url,Place CSRF Token in the URL>>
@@ -351,17 +350,9 @@ Before you integrate Spring Security's CSRF protection with multipart file uploa
More information about using multipart forms with Spring, see the https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web.html#mvc-multipart[1.1.11. Multipart Resolver] section of the Spring reference and the https://docs.spring.io/spring/docs/5.2.x/javadoc-api/org/springframework/web/multipart/support/MultipartFilter.html[`MultipartFilter` Javadoc].
====
[[csrf-considerations-multipart-header]]
==== Include CSRF Token in an HTTP Request Header
When JavaScript is available, you can submit multipart requests by sending the CSRF token in an HTTP request header.
This avoids placing the token in the URL and avoids processing multipart request bodies before CSRF validation.
This is generally the preferred approach for browser applications with JavaScript clients.
See xref:servlet/exploits/csrf.adoc#csrf-integration-javascript-other[JavaScript applications] for Servlet applications and xref:reactive/exploits/csrf.adoc#webflux-csrf-include-ajax[AJAX and JSON Requests] for Reactive applications.
[[csrf-considerations-multipart-body]]
==== Place CSRF Token in the Body
Another option is to include the actual CSRF token in the body of the request.
The first option is to include the actual CSRF token in the body of the request.
By placing the CSRF token in the body, the body is read before authorization is performed.
This means that anyone can place temporary files on your server.
However, only authorized users can submit a file that is processed by your application.
@@ -369,7 +360,7 @@ In general, this is the recommended approach, because the temporary file upload
[[csrf-considerations-multipart-url]]
==== Include CSRF Token in URL
If letting unauthorized users upload temporary files is not acceptable and JavaScript is not available, an alternative is to include the expected CSRF token as a query parameter in the action attribute of the form.
If letting unauthorized users upload temporary files is not acceptable, an alternative is to include the expected CSRF token as a query parameter in the action attribute of the form.
The disadvantage to this approach is that query parameters can be leaked.
More generally, it is considered best practice to place sensitive data within the body or headers to ensure it is not leaked.
You can find additional information in https://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3[RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI's].
@@ -1,4 +0,0 @@
= Kerberos Migrations
For users leveraging Spring Security's Kerberos support, the Maven and Gradle Coordinates have been changed since the support was moved from an external module into Spring Security.
See the xref:servlet/authentication/kerberos/introduction.adoc[Keberos documentation] for the new Maven and Gradle coordinates.
@@ -391,8 +391,7 @@ For details, see the <<webflux-csrf-configure-custom-repository>> section.
[[webflux-csrf-considerations-multipart]]
=== Multipart (file upload)
We have xref:features/exploits/csrf.adoc#csrf-considerations-multipart[already discussed] how protecting multipart requests (file uploads) from CSRF attacks causes a https://en.wikipedia.org/wiki/Chicken_or_the_egg[chicken and the egg] problem.
When JavaScript is available, we _recommend_ including the CSRF token in an HTTP request header (see <<webflux-csrf-include-ajax,AJAX and JSON Requests>>) to side-step the issue.
If JavaScript is not available, this section discusses how to place the CSRF token in the <<webflux-csrf-considerations-multipart-body,body>> and <<webflux-csrf-considerations-multipart-url,url>> within a WebFlux application.
This section discusses how to implement placing the CSRF token in the <<webflux-csrf-considerations-multipart-body,body>> and <<webflux-csrf-considerations-multipart-url,url>> within a WebFlux application.
[NOTE]
====
+295 -14
View File
@@ -51,9 +51,28 @@ image::{figures}/delegatingfilterproxy.png[]
The following listing shows pseudo code of `DelegatingFilterProxy`:
.`DelegatingFilterProxy` Pseudo Code
[tabs]
======
Java::
+
[source,java,role="primary"]
----
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); // <1>
delegate.doFilter(request, response); // <2>
}
----
include-code::./SampleDelegatingFilterProxy[tag=dofilter,indent=0]
Kotlin::
+
[source,kotlin,role="secondary"]
----
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val delegate: Filter = getFilterBean(someBeanName) // <1>
delegate.doFilter(request, response) // <2>
}
----
======
<1> Lazily get Filter that was registered as a Spring Bean.
For the example in <<servlet-delegatingfilterproxy-figure>> `delegate` is an instance of __Bean Filter~0~__.
<2> Delegate work to the Spring Bean.
@@ -136,7 +155,58 @@ However, there are times that it is beneficial to know the ordering, if you want
These security filters are most often declared using an javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[`HttpSecurity`] instance.
To exemplify the above paragraph, let's consider the following security configuration:
include-code::./SecurityConfig[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
----
======
The above configuration will result in the following `Filter` ordering:
@@ -163,7 +233,7 @@ If you want to see the list of filters invoked for a particular request, you can
=== Printing the Security Filters
Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request.
For example, you want to make sure that the <<adding-filters-to-chain,filter you have added>> is in the list of the security filters.
For example, you want to make sure that the <<adding-custom-filter,filter you have added>> is in the list of the security filters.
The list of filters is printed at DEBUG level on the application startup, so you can see something like the following on the console output for example:
@@ -178,7 +248,7 @@ But that is not all, you can also configure your application to print the invoca
That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from.
To do that, you can configure your application to <<servlet-logging,log the security events>>.
[[adding-filters-to-chain]]
[[adding-custom-filter]]
=== Adding Filters to the Filter Chain
Most of the time, the default <<servlet-security-filters>> are enough to provide security to your application.
@@ -190,7 +260,6 @@ javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity
* `#addFilterAfter(Filter, Class<?>)` adds your filter after another filter
* `#addFilterAt(Filter, Class<?>)` replaces another filter with your filter
[[adding-custom-filter]]
==== Adding a Custom Filter
If you are creating a filter of your own, you will need to determine its location in the filter chain.
@@ -229,7 +298,39 @@ For example, let's say that you want to add a `Filter` that gets a tenant id hea
First, let's create the `Filter`:
include-code::./TenantFilter[tag=snippet,indent=0]
[source,java]
----
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); <1>
boolean hasAccess = isUserAllowed(tenantId); <2>
if (hasAccess) {
filterChain.doFilter(request, response); <3>
return;
}
throw new AccessDeniedException("Access denied"); <4>
}
}
----
The sample code above does the following:
@@ -248,7 +349,34 @@ The previous description already gives us a clue on where to add the filter, sin
Based on the rule of thumb, add it after xref:servlet/authentication/anonymous.adoc[ `AnonymousAuthenticationFilter`], the last authentication filter in the chain, like so:
include-code::./SecurityConfig[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); <1>
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) <1>
return http.build()
}
----
======
<1> Use `HttpSecurity#addFilterAfter` to add the `TenantFilter` after the `AnonymousAuthenticationFilter`.
@@ -277,23 +405,127 @@ public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilte
This makes so that `HttpSecurity` is the only one adding it.
[[customizing-filter]]
==== Customizing a Spring Security Filter
Generally, you can use a filter's DSL method to configure Spring Security's filters.
For example, the simplest way to add `BasicAuthenticationFilter` is by asking the DSL to do it:
include-code::./CustomizingFilterTests[tag=basic-default,indent=0]
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults())
// ...
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic { }
// ...
}
return http.build()
}
----
======
However, in the event that you want to construct a Spring Security filter yourself, you specify it in the DSL using `addFilterAt` like so:
include-code::./CustomizingFilterTests[tag=custom-filter,indent=0]
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
----
======
Note that if that filter has already been added, then Spring Security will throw an exception.
For example, calling xref:servlet/authentication/passwords/basic.adoc[ `HttpSecurity#httpBasic`] adds a `BasicAuthenticationFilter` for you.
So, the following arrangement fails since there are two calls that are both trying to add `BasicAuthenticationFilter`:
include-code::./CustomizingFilterTests[tag=incorrect,indent=0]
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
.httpBasic(Customizer.withDefaults())
// ... on no! BasicAuthenticationFilter is added twice!
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http {
httpBasic { }
}
// ... on no! BasicAuthenticationFilter is added twice!
http.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
----
======
In this case, remove the call to `httpBasic` since you are constructing `BasicAuthenticationFilter` yourself.
@@ -301,7 +533,10 @@ In this case, remove the call to `httpBasic` since you are constructing `BasicAu
====
In the event that you are unable to reconfigure `HttpSecurity` to not add a certain filter, you can typically disable the Spring Security filter by calling its DSL's `disable` method like so:
include-code::./CustomizingFilterTests[tag=disable,indent=0]
[source,java]
----
.httpBasic((basic) -> basic.disable())
----
====
[[servlet-exceptiontranslationfilter]]
@@ -383,7 +618,53 @@ Or you may want to shut off this feature since you always want to redirect the u
To do that, you can use the javadoc:org.springframework.security.web.savedrequest.NullRequestCache[NullRequestCache] implementation.
.Prevent the Request From Being Saved
include-code::./SecurityConfig[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
----
XML::
+
[source,xml,role="secondary"]
----
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
----
======
[[requestcacheawarefilter]]
=== RequestCacheAwareFilter
@@ -3,38 +3,3 @@
Spring Security Kerberos {spring-security-version} is built and tested with JDK 17,
Spring Security {spring-security-version} and Spring Framework {spring-core-version}.
The dependency coordinates changed with Spring Security 7:
[tabs]
======
Maven::
+
.pom.xml
[source,xml,subs="verbatim,attributes"]
----
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-kerberos-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-kerberos-web</artifactId>
</dependency>
</dependencies>
----
Gradle::
+
.build.gradle
[source,groovy]
[subs="verbatim,attributes"]
----
dependencies {
implementation "org.springframework.security:spring-security-kerberos-core"
implementation "org.springframework.security:spring-security-kerberos-web"
}
----
======
@@ -949,21 +949,6 @@ fun getMessages(): List<Message> { }
----
======
[[method-security-has-scope]]
=== Using `hasScope` in Method Security
Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:
include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]
and then doing:
include-code::./MessageService[tag=protected-method,indent=0]
If you are using xref:servlet/authentication/mfa.adoc[Spring Security's MFA feature], then you can supply its `AuthorizationManagerFactory` instance to ensure that your authentication factors are automatically checked as well by including it in your `DefaultOAuth2AuthorizationManagerFactory` constructor as follows:
include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]
[[oauth2resourceserver-jwt-authorization-extraction]]
=== Extracting Authorities Manually
@@ -638,21 +638,6 @@ fun getMessages(): List<Message?> {}
----
======
[[method-security-has-scope]]
=== Using `hasScope` in Method Security
Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:
include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]
and then doing:
include-code::./MessageService[tag=protected-method,indent=0]
If you are using xref:servlet/authentication/mfa.adoc[Spring Security's MFA feature], then you can supply its `AuthorizationManagerFactory` instance to ensure that your authentication factors are automatically checked as well by including it in your `DefaultOAuth2AuthorizationManagerFactory` constructor as follows:
include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]
[[oauth2resourceserver-opaque-authorization-extraction]]
=== Extracting Authorities Manually
+1 -1
View File
@@ -1,4 +1,4 @@
[[new]]
= What's New in Spring Security 7.1
* https://github.com/spring-projects/spring-security/pull/18634[gh-18634] - Added javadoc:org.springframework.security.web.util.matcher.InetAddressMatcher[]
This is a placeholder for updates to Spring Security 7.1
-1
View File
@@ -6,7 +6,6 @@ plugins {
id 'java-toolchain'
id 'test-compile-target-jdk25'
id 'compile-warnings-error'
id 'javadoc-warnings-error'
}
apply plugin: 'io.spring.convention.docs'
@@ -19,6 +19,7 @@ package org.springframework.security.docs.features.authentication.password4jbcry
import com.password4j.BcryptFunction;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder;
@@ -32,7 +33,7 @@ public class BcryptUsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
PasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
@@ -1,102 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.addingcustomfilter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
CustomFilterTests.UserDetailsConfig.class,
CustomFilterTests.ApiController.class,
SecurityConfig.class })
@WebAppConfiguration
public class CustomFilterTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
void setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
.defaultRequest(get("/api").with(user("user")))
.apply(springSecurity())
.build();
}
@Test
void tenantFilterWhenHeaderMissingThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.mvc.perform(get("/api")).andReturn());
}
@Test
void tenantFilterWhenHeaderPresentThenContinuesFilterChain() throws Exception {
this.mvc.perform(get("/api").header("X-Tenant-Id", "some-tenant-id"))
.andExpect(status().isOk())
.andExpect(authenticated().withUsername("user"));
}
@Configuration
static class UserDetailsConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
@RestController
static class ApiController {
@GetMapping("/api")
String api() {
return "ok";
}
}
}
@@ -1,42 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.addingcustomfilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.test.context.ContextConfiguration;
@Configuration
@ContextConfiguration(classes = { SecurityConfig.class })
@EnableWebSecurity
public class SecurityConfig {
// tag::snippet[]
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); // <1>
return http.build();
}
// end::snippet[]
}
@@ -1,53 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.addingcustomfilter;
// tag::snippet[]
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); // <1>
boolean hasAccess = isUserAllowed(tenantId); // <2>
if (hasAccess) {
filterChain.doFilter(request, response); // <3>
return;
}
throw new AccessDeniedException("Access denied"); // <4>
}
private boolean isUserAllowed(String tenantId) {
return "some-tenant-id".equals(tenantId);
}
}
// end::snippet[]
@@ -1,203 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.customizingfilter;
import java.io.IOException;
import java.util.List;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
/**
* Tests for customizing security filters.
*
*/
@ExtendWith(SpringTestContextExtension.class)
public class CustomizingFilterTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
MockMvc mvc;
@Autowired
FilterChainProxy filterChainProxy;
@Test
public void filterChainWhenBasicDefaultThenBasicAuthenticationFilterPresent() {
this.spring.register(SecurityConfigBasicDefault.class).autowire();
List<Filter> filters = this.filterChainProxy.getFilters("/");
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter.class);
}
@Test
public void filterChainWhenCustomFilterThenCustomFilterPresent() {
this.spring.register(SecurityConfigCustom.class).autowire();
List<Filter> filters = this.filterChainProxy.getFilters("/");
assertThat(filters).extracting("class").contains(SecurityConfigCustom.MyBasicAuthenticationFilter.class);
assertThat(filters).extracting("class").doesNotContain(BasicAuthenticationFilter.class);
}
@Test
public void requestWhenDisableThenNoWwwAuthenticateHeader() throws Exception {
this.spring.register(SecurityConfigDisable.class).autowire();
this.mvc.perform(get("/")).andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
}
@Test
public void filterChainWhenIncorrectThenBothFiltersPresent() {
this.spring.register(SecurityConfigIncorrect.class).autowire();
List<Filter> filters = this.filterChainProxy.getFilters("/");
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter.class);
assertThat(filters).extracting("class").contains(SecurityConfigIncorrect.MyBasicAuthenticationFilter.class);
}
@Configuration
@EnableWebSecurity
static class SecurityConfigBasicDefault {
// tag::basic-default[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults());
// ...
return http.build();
}
// end::basic-default[]
}
@Configuration
@EnableWebSecurity
static class SecurityConfigCustom {
// tag::custom-filter[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
MyBasicAuthenticationFilter basic = new MyBasicAuthenticationFilter();
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
// end::custom-filter[]
static class MyBasicAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
}
}
}
@Configuration
@EnableWebSecurity
static class SecurityConfigDisable {
// tag::disable[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic((basic) -> basic.disable());
// ...
return http.build();
}
// end::disable[]
}
@Configuration
@EnableWebSecurity
static class SecurityConfigIncorrect {
// tag::incorrect[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
MyBasicAuthenticationFilter basic = new MyBasicAuthenticationFilter();
// ... configure
http
.httpBasic(Customizer.withDefaults())
// ... on no! BasicAuthenticationFilter is added twice!
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
// end::incorrect[]
static class MyBasicAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
}
}
}
@Configuration
static class UserDetailsConfig {
@Bean
InMemoryUserDetailsManager userDetailsManager() {
return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build());
}
}
}
@@ -1,16 +0,0 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
class MessageService {
// tag::protected-method[]
@PreAuthorize("@oauth2.hasScope('message:read')")
String readMessage() {
return "message";
}
// end::protected-method[]
}
@@ -1,18 +0,0 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;
@Configuration
@EnableMethodSecurity
class MethodSecurityHasScopeConfiguration {
// tag::declare-factory[]
@Bean
OAuth2AuthorizationManagerFactory<?> oauth2() {
return new DefaultOAuth2AuthorizationManagerFactory<>();
}
// end::declare-factory[]
}
@@ -1,59 +0,0 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ExtendWith(SpringTestContextExtension.class)
@ExtendWith(SpringExtension.class)
@SecurityTestExecutionListeners
public class MethodSecurityHasScopeConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this).mockMvcAfterSpringSecurityOk();
@Autowired
private MessageService messages;
@Test
@WithMockUser(authorities = "SCOPE_message:read")
void readMessageWhenMessageReadThenAllowed() {
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
this.messages.readMessage();
}
@Test
@WithMockUser
void readMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
@Test
@WithMockUser(authorities = { "SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509" })
void mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
this.messages.readMessage();
}
@Test
@WithMockUser(authorities = { "SCOPE_message:read" })
void mfaReadMessageWhenMessageReadThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
@Test
@WithMockUser
void mfaReadMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
}
@@ -1,21 +0,0 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;
@Configuration
@EnableMethodSecurity
@EnableMultiFactorAuthentication(authorities = { "FACTOR_BEARER", "FACTOR_X509" })
class MethodSecurityHasScopeMfaConfiguration {
// tag::declare-factory[]
@Bean
OAuth2AuthorizationManagerFactory<?> oauth2(AuthorizationManagerFactory<?> authz) {
return new DefaultOAuth2AuthorizationManagerFactory<>(authz);
}
// end::declare-factory[]
}
@@ -1,40 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.requestcachepreventsavedrequest;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
public class SecurityConfig {
// tag::snippet[]
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
// end::snippet[]
}
@@ -1,53 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.servletdelegatingfilterproxy;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
/**
* A very simple implementation of a DelegatingFilterProxy.
*/
public class SampleDelegatingFilterProxy extends GenericFilterBean {
private StaticWebApplicationContext wac;
private final String someBeanName;
public SampleDelegatingFilterProxy(String someBeanName, StaticWebApplicationContext webApplicationContext) {
this.wac = webApplicationContext;
this.someBeanName = someBeanName;
}
// tag::dofilter[]
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Filter delegate = getFilterBean(someBeanName); // <1>
delegate.doFilter(request, response, chain); // <2>
}
// end::dofilter[]
private Filter getFilterBean(String someBeanName) {
return this.wac.getBean(someBeanName, Filter.class);
}
}
@@ -1,129 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.servletdelegatingfilterproxy;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.web.servlet.MockServletContext;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.StaticWebApplicationContext;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class SampleDelegatingFilterProxyTests {
@Test
void testFilter() throws ServletException, IOException {
ServletContext sc = new MockServletContext();
StaticWebApplicationContext wac = new StaticWebApplicationContext();
wac.registerSingleton("targetFilter", MockFilter.class);
wac.setServletContext(sc);
wac.refresh();
sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);
MockFilter targetFilter = (MockFilter) wac.getBean("targetFilter");
MockFilterConfig proxyConfig = new MockFilterConfig(sc);
proxyConfig.addInitParameter("targetBeanName", "targetFilter");
SampleDelegatingFilterProxy filterProxy = new SampleDelegatingFilterProxy("targetFilter", wac);
filterProxy.init(proxyConfig);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
filterProxy.doFilter(request, response, null);
assertThat(targetFilter.filterConfig).isNull();
assertThat(request.getAttribute("called")).isEqualTo(Boolean.TRUE);
filterProxy.destroy();
assertThat(targetFilter.filterConfig).isNull();
}
private static class MockFilter implements Filter {
private FilterConfig filterConfig;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws java.io.IOException, ServletException {
request.setAttribute("called", Boolean.TRUE);
}
@Override
public void destroy() {
this.filterConfig = null;
}
}
private static class MockFilterConfig implements FilterConfig {
private final ServletContext servletContext;
private final String filterName;
private final Map<String, String> initParameters = new LinkedHashMap<>();
public MockFilterConfig(ServletContext servletContext) {
this.servletContext = servletContext;
this.filterName = "";
}
@Override
public String getFilterName() {
return this.filterName;
}
@Override
public ServletContext getServletContext() {
return this.servletContext;
}
public void addInitParameter(String name, String value) {
Assert.notNull(name, "Parameter name must not be null");
this.initParameters.put(name, value);
}
@Override
public String getInitParameter(String name) {
Assert.notNull(name, "Parameter name must not be null");
return this.initParameters.get(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return Collections.enumeration(this.initParameters.keySet());
}
}
}
@@ -1,96 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.servletsecurityfilters;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { SampleSecurityConfigTests.UserDetailsConfig.class, SecurityConfig.class })
@WebAppConfiguration
public class SampleSecurityConfigTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
public void setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
.defaultRequest(get("/api").with(user("user")))
.defaultRequest(post("/api").with(csrf()))
.apply(springSecurity())
.build();
}
@Test
void testGet() throws Exception {
this.mvc.perform(get("/api")
.with(httpBasic("user", "password")))
// Security check was successful
.andExpect(status().isNotFound())
.andExpect(authenticated().withUsername("user"));
}
@Test
void testUnauthenticated() throws Exception {
this.mvc.perform(get("/api"))
// Security check was successful
.andExpect(status().isUnauthorized());
}
@Test
void testCsrf() throws Exception {
this.mvc.perform(post("/api")
.with(csrf())
.with(httpBasic("user", "password"))
).andExpect(status().isNotFound());
}
@Configuration
static class UserDetailsConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.build();
return new InMemoryUserDetailsManager(user);
}
}
}
@@ -1,45 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.servletsecurityfilters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::snippet[]
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
// end::snippet[]
@@ -3,6 +3,7 @@ package org.springframework.security.kt.docs.features.authentication.password4jb
import com.password4j.BcryptFunction
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder
@@ -13,7 +14,7 @@ class BcryptUsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder()
val encoder: PasswordEncoder = BCryptPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
@@ -1,106 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.addingcustomfilter
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@ExtendWith(SpringExtension::class)
@ContextConfiguration(
classes = [
CustomFilterTests.UserDetailsConfig::class,
CustomFilterTests.ApiController::class,
SecurityConfig::class
]
)
@WebAppConfiguration
class CustomFilterTests {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mvc: MockMvc
@BeforeEach
fun setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build();
}
@Test
fun tenantFilterWhenHeaderMissingThenAccessDenied() {
assertThatExceptionOfType(Exception::class.java)
.isThrownBy { this.mvc.perform(get("/api").with(user("user"))).andReturn() }
}
@Test
fun tenantFilterWhenHeaderPresentThenContinuesFilterChain() {
this.mvc.perform(get("/api")
.with(user("user"))
.header("X-Tenant-Id", "some-tenant-id"))
.andExpect(status().isOk)
.andExpect(authenticated().withUsername("user"))
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): UserDetailsService {
@Suppress("DEPRECATION")
val user: UserDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
}
@RestController
class ApiController {
@GetMapping("/api")
fun api(): String {
return "ok"
}
}
}
@@ -1,40 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.addingcustomfilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter
@Configuration
@EnableWebSecurity
open class SecurityConfig {
// tag::snippet[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) // <1>
return http.build()
}
// end::snippet[]
}
@@ -1,51 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.addingcustomfilter
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import java.io.IOException
// tag::snippet[]
class TenantFilter : Filter {
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse
val tenantId = request.getHeader("X-Tenant-Id") // <1>
val hasAccess = isUserAllowed(tenantId) // <2>
if (hasAccess) {
filterChain.doFilter(request, response) // <3>
return
}
throw AccessDeniedException("Access denied") // <4>
}
private fun isUserAllowed(tenantId: String?): Boolean {
return "some-tenant-id" == tenantId
}
}
// end::snippet[]
@@ -1,167 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.customizingfilter
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.test.web.servlet.MockMvc
/**
* Tests for customizing security filters.
*
*/
@ExtendWith(SpringTestContextExtension::class)
class CustomizingFilterTests {
@JvmField
val spring = SpringTestContext(this)
@Autowired
lateinit var mvc: MockMvc
@Autowired
lateinit var filterChainProxy: FilterChainProxy
@Test
fun `filter chain when basic default then BasicAuthenticationFilter present`() {
spring.register(SecurityConfigBasicDefault::class.java).autowire()
val filters = filterChainProxy.getFilters("/")
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter::class.java)
}
@Test
fun `filter chain when custom filter then custom filter present`() {
spring.register(SecurityConfigCustom::class.java).autowire()
val filters = filterChainProxy.getFilters("/")
assertThat(filters).extracting("class").contains(SecurityConfigCustom.MyBasicAuthenticationFilter::class.java)
assertThat(filters).extracting("class").doesNotContain(BasicAuthenticationFilter::class.java)
}
@Test
fun `filter chain when incorrect then both filters present`() {
spring.register(SecurityConfigIncorrect::class.java).autowire()
val filters = filterChainProxy.getFilters("/")
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter::class.java)
assertThat(filters).extracting("class").contains(SecurityConfigIncorrect.MyBasicAuthenticationFilter::class.java)
}
@Configuration
@EnableWebSecurity
open class SecurityConfigBasicDefault {
// tag::basic-default[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic { }
// ...
}
return http.build()
}
// end::basic-default[]
}
@Configuration
@EnableWebSecurity
open class SecurityConfigCustom {
// tag::custom-filter[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = MyBasicAuthenticationFilter()
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
// end::custom-filter[]
class MyBasicAuthenticationFilter : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
chain.doFilter(request, response)
}
}
}
@Configuration @EnableWebSecurity
open class SecurityConfigDisable {
// tag::disable[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic {
disable()
}
// ...
}
return http.build()
}
// end::disable[]
}
@Configuration
@EnableWebSecurity
open class SecurityConfigIncorrect {
// tag::incorrect[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = MyBasicAuthenticationFilter()
// ... configure
http {
httpBasic { }
}
// ... on no! BasicAuthenticationFilter is added twice!
http.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
// end::incorrect[]
class MyBasicAuthenticationFilter : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
chain.doFilter(request, response)
}
}
}
}
@@ -1,15 +0,0 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Service
@Service
open class MessageService {
// tag::protected-method[]
@PreAuthorize("@oauth2.hasScope('message:read')")
open fun readMessage(): String {
return "message"
}
// end::protected-method[]
}
@@ -1,18 +0,0 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory
@Configuration
@EnableMethodSecurity
open class MethodSecurityHasScopeConfiguration {
// tag::declare-factory[]
@Bean
open fun oauth2(): OAuth2AuthorizationManagerFactory<Any> {
return DefaultOAuth2AuthorizationManagerFactory()
}
// end::declare-factory[]
}
@@ -1,61 +0,0 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners
import org.springframework.security.test.context.support.WithMockUser
import org.springframework.test.context.junit.jupiter.SpringExtension
@ExtendWith(SpringTestContextExtension::class)
@ExtendWith(SpringExtension::class)
@SecurityTestExecutionListeners
class MethodSecurityHasScopeConfigurationTests {
@JvmField
val spring: SpringTestContext = SpringTestContext(this).mockMvcAfterSpringSecurityOk()
@Autowired
var messages: MessageService? = null
@Test
@WithMockUser(authorities = ["SCOPE_message:read"])
fun readMessageWhenMessageReadThenAllowed() {
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
this.messages!!.readMessage()
}
@Test
@WithMockUser
fun readMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
@Test
@WithMockUser(authorities = ["SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509"])
fun mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
this.messages!!.readMessage()
}
@Test
@WithMockUser(authorities = ["SCOPE_message:read"])
fun mfaReadMessageWhenMessageReadThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
@Test
@WithMockUser
fun mfaReadMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
}
@@ -1,20 +0,0 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authorization.AuthorizationManagerFactory
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory
@Configuration
@EnableMethodSecurity
@EnableMultiFactorAuthentication(authorities = ["FACTOR_BEARER", "FACTOR_X509"])
open class MethodSecurityHasScopeMfaConfiguration {
// tag::declare-factory[]
@Bean
open fun oauth2(authz: AuthorizationManagerFactory<Any>): OAuth2AuthorizationManagerFactory<Any> {
return DefaultOAuth2AuthorizationManagerFactory(authz)
} // end::declare-factory[]
}
@@ -1,40 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.requestcachepreventsavedrequest
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.savedrequest.NullRequestCache
open class SecurityConfig {
// tag::snippet[]
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
// end::snippet[]
}
@@ -1,49 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.servletdelegatingfilterproxy;
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
/**
* A very simple implementation of a DelegatingFilterProxy.
*/
class SampleDelegatingFilterProxy(
private val someBeanName: String,
private var wac: StaticWebApplicationContext
) : GenericFilterBean() {
// tag::dofilter[]
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain?) {
val delegate: Filter = getFilterBean(someBeanName) // <1>
delegate.doFilter(request, response, chain) // <2>
}
// end::dofilter[]
private fun getFilterBean(someBeanName: String): Filter {
return wac.getBean(someBeanName, Filter::class.java)
}
}
@@ -1,109 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.servletdelegatingfilterproxy
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.FilterConfig
import jakarta.servlet.ServletContext
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.junit.jupiter.api.Test
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.web.servlet.MockServletContext
import org.springframework.util.Assert
import org.springframework.web.context.WebApplicationContext
import org.springframework.web.context.support.StaticWebApplicationContext
import java.io.IOException
import org.assertj.core.api.Assertions.assertThat
import java.util.Enumeration
import java.util.Collections
import kotlin.collections.LinkedHashMap
class SampleDelegatingFilterProxyTests {
@Test
@Throws(ServletException::class, IOException::class)
fun testFilter() {
val sc: ServletContext = MockServletContext()
val wac = StaticWebApplicationContext()
wac.registerSingleton("targetFilter", MockFilter::class.java)
wac.setServletContext(sc)
wac.refresh()
sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac)
val targetFilter = wac.getBean("targetFilter") as MockFilter
val proxyConfig = MockFilterConfig(sc)
proxyConfig.addInitParameter("targetBeanName", "targetFilter")
val filterProxy = SampleDelegatingFilterProxy("targetFilter", wac)
filterProxy.init(proxyConfig)
val request = MockHttpServletRequest()
val response = MockHttpServletResponse()
filterProxy.doFilter(request, response, null)
assertThat(targetFilter.filterConfig).isNull()
assertThat(request.getAttribute("called")).isEqualTo(true)
filterProxy.destroy()
assertThat(targetFilter.filterConfig).isNull()
}
private class MockFilter : Filter {
var filterConfig: FilterConfig? = null
@Throws(ServletException::class)
override fun init(filterConfig: FilterConfig) {
this.filterConfig = filterConfig
}
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain?) {
request.setAttribute("called", true)
}
override fun destroy() {
filterConfig = null
}
}
private class MockFilterConfig(
private val servletContext: ServletContext
) : FilterConfig {
private val filterName: String = ""
private val initParameters = LinkedHashMap<String, String>()
override fun getFilterName(): String = filterName
override fun getServletContext(): ServletContext = servletContext
fun addInitParameter(name: String, value: String) {
Assert.notNull(name, "Parameter name must not be null")
initParameters[name] = value
}
override fun getInitParameter(name: String): String? {
Assert.notNull(name, "Parameter name must not be null")
return initParameters[name]
}
override fun getInitParameterNames(): Enumeration<String> =
Collections.enumeration(initParameters.keys)
}
}
@@ -1,45 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.servletsecurityfilters
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
// tag::snippet[]
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
// end::snippet[]
@@ -1,34 +0,0 @@
<?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"
xmlns:p="http://www.springframework.org/schema/p"
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">
<!-- tag::snippet[] -->
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
<!-- end::snippet[] -->
</b:beans>
+18 -18
View File
@@ -4,39 +4,39 @@ io-rsocket = "1.1.5"
io-spring-javaformat = "0.0.47"
io-spring-nohttp = "0.0.11"
jakarta-websocket = "2.2.0"
org-apache-maven-resolver = "1.9.27"
org-apache-maven-resolver = "1.9.25"
org-aspectj = "1.9.25.1"
org-bouncycastle = "1.83"
org-eclipse-jetty = "11.0.26"
org-jetbrains-kotlin = "2.3.10"
org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.21.0"
org-opensaml5 = "5.2.1"
org-springframework = "7.0.5"
org-opensaml5 = "5.2.0"
org-springframework = "7.0.3"
com-password4j = "1.8.4"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.21.1"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.28"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.21.0"
com-google-inject-guice = "com.google.inject:guice:3.0"
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:10.6"
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:11.33"
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:10.4"
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:11.26.1"
com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" }
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:7.0.4"
com-jayway-jsonpath-json-path = "com.jayway.jsonpath:json-path:2.10.0"
commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.2.1"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.16.3"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.16.2"
io-mockk = "io.mockk:mockk:1.14.9"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.3"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.2"
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" }
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.14"
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.13"
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:3.0.0"
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.2.0"
@@ -65,27 +65,27 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.2.5.Final"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.2.4.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-junit-junit-bom = "org.junit:junit-bom:6.0.3"
org-junit-junit-bom = "org.junit:junit-bom:6.0.2"
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
org-opensaml-opensaml5-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml5" }
org-opensaml-opensaml5-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml5" }
org-python-jython = { module = "org.python:jython", version = "2.5.3" }
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit3-driver:4.41.0"
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.41.0"
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit3-driver:4.40.0"
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.40.0"
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.3"
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.2"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.1"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.1.0"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.4"
com-google-code-gson-gson = "com.google.code.gson:gson:2.13.2"
com-thaiopensource-trag = "com.thaiopensource:trang:20091111"
@@ -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.11'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.0.RELEASE'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.7.RELEASE'
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
[plugins]
@@ -1,43 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.aot.hint;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
/**
* {@link RuntimeHintsRegistrar} for Kerberos authentication classes.
*
* @author Josh Long
*/
class KerberosRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
// Krb5LoginModule is referenced as a plain string in LoginConfig and loaded by
// JAAS via Class.forName at runtime, so it needs an explicit reflection hint.
hints.reflection()
.registerType(TypeReference.of("com.sun.security.auth.module.Krb5LoginModule"),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS));
}
}
@@ -1,23 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* AOT and native image hint support for Kerberos authentication.
*/
@NullMarked
package org.springframework.security.kerberos.aot.hint;
import org.jspecify.annotations.NullMarked;
@@ -1,2 +0,0 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.security.kerberos.aot.hint.KerberosRuntimeHints
@@ -30,7 +30,6 @@ import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -113,11 +112,6 @@ public class JwtClientAssertionAuthenticationProviderTests {
.setContext(new TestAuthorizationServerContext(this.authorizationServerSettings, null));
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
@@ -25,7 +25,6 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -112,11 +111,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
@@ -22,7 +22,6 @@ import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -96,11 +95,6 @@ public class OAuth2AuthorizationConsentAuthenticationProviderTests {
.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
@@ -25,7 +25,6 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -97,11 +96,6 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests {
mockAuthorizationServerContext();
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryIsNullThenThrowIllegalArgumentException() {
// @formatter:off
@@ -254,8 +254,6 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
private Duration clockSkew;
private Clock clock;
@@ -276,21 +274,6 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
return this;
}
/**
* Sets a {@link ReactiveOAuth2AuthorizationSuccessHandler} to use for handling
* successful refresh token response, defaults to
* {@link RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler}.
* @param authorizationSuccessHandler the
* {@link ReactiveOAuth2AuthorizationSuccessHandler} to use
* @return the {@link RefreshTokenGrantBuilder}
* @since 7.1
*/
public RefreshTokenGrantBuilder authorizationSuccessHandler(
ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
this.authorizationSuccessHandler = authorizationSuccessHandler;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if
@@ -327,9 +310,6 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.authorizationSuccessHandler != null) {
authorizedClientProvider.setAuthorizationSuccessHandler(this.authorizationSuccessHandler);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
@@ -1,305 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Duration;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@link ReactiveOAuth2AuthorizationSuccessHandler} that refreshes an {@link OidcUser}
* in the {@link SecurityContext} if the refreshed {@link OidcIdToken} is valid according
* to <a href=
* "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse">OpenID
* Connect Core 1.0 - Section 12.2 Successful Refresh Response</a>
*
* @author Evgeniy Cheban
* @since 7.1
* @see RefreshTokenReactiveOAuth2AuthorizedClientProvider
*/
public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
implements ReactiveOAuth2AuthorizationSuccessHandler {
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private static final String REFRESH_TOKEN_RESPONSE_ERROR_URI = "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse";
// @formatter:off
private static final Mono<ServerWebExchange> currentServerWebExchangeMono = Mono.deferContextual(Mono::just)
.filter((c) -> c.hasKey(ServerWebExchange.class))
.map((c) -> c.get(ServerWebExchange.class));
// @formatter:on
private ServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
private ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = new OidcReactiveOAuth2UserService();
private GrantedAuthoritiesMapper authoritiesMapper = (authorities) -> authorities;
private Duration clockSkew = Duration.ofSeconds(60);
@Override
public Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal,
Map<String, Object> attributes) {
// The response must contain the openid scope.
if (!authorizedClient.getAccessToken().getScopes().contains(OidcScopes.OPENID)) {
return Mono.empty();
}
// The response must contain an id_token.
String idToken = extractIdToken(attributes);
if (!StringUtils.hasText(idToken)) {
return Mono.empty();
}
if (!(principal instanceof OAuth2AuthenticationToken authenticationToken)
|| authenticationToken.getClass() != OAuth2AuthenticationToken.class) {
// If the application customizes the authentication result, then a custom
// handler should be provided.
return Mono.empty();
}
// The current principal must be an OidcUser.
if (!(authenticationToken.getPrincipal() instanceof OidcUser existingOidcUser)) {
return Mono.empty();
}
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
// The registrationId must match the one used to log in.
if (!authenticationToken.getAuthorizedClientRegistrationId().equals(clientRegistration.getRegistrationId())) {
return Mono.empty();
}
// Create, validate OidcIdToken and refresh OidcUser in the SecurityContext.
return Mono.justOrEmpty((ServerWebExchange) attributes.get(ServerWebExchange.class.getName()))
.switchIfEmpty(currentServerWebExchangeMono)
.flatMap((exchange) -> {
ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
return jwtDecoder.decode(idToken).onErrorMap(JwtException.class, (ex) -> {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(),
null);
return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
})
.map((jwt) -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims()))
.doOnNext((oidcIdToken) -> validateIdToken(existingOidcUser, oidcIdToken))
.flatMap((oidcIdToken) -> {
OidcUserRequest userRequest = new OidcUserRequest(clientRegistration,
authorizedClient.getAccessToken(), oidcIdToken);
return this.userService.loadUser(userRequest);
})
.flatMap((oidcUser) -> refreshSecurityContext(exchange, clientRegistration, authenticationToken,
oidcUser));
});
}
/**
* Sets a {@link ServerSecurityContextRepository} to use for refreshing a
* {@link SecurityContext}, defaults to
* {@link WebSessionServerSecurityContextRepository}.
* @param serverSecurityContextRepository the {@link ServerSecurityContextRepository}
* to use
*/
public void setServerSecurityContextRepository(ServerSecurityContextRepository serverSecurityContextRepository) {
Assert.notNull(serverSecurityContextRepository, "serverSecurityContextRepository cannot be null");
this.serverSecurityContextRepository = serverSecurityContextRepository;
}
/**
* Sets a {@link ReactiveJwtDecoderFactory} to use for decoding refreshed oidc
* id-token, defaults to {@link ReactiveOidcIdTokenDecoderFactory}.
* @param jwtDecoderFactory the {@link ReactiveJwtDecoderFactory} to use
*/
public void setJwtDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
this.jwtDecoderFactory = jwtDecoderFactory;
}
/**
* Sets a {@link ReactiveOAuth2UserService} to use for loading an {@link OidcUser}
* from refreshed oidc id-token, defaults to {@link OidcReactiveOAuth2UserService}.
* @param userService the {@link ReactiveOAuth2UserService} to use
*/
public void setUserService(ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService) {
Assert.notNull(userService, "userService cannot be null");
this.userService = userService;
}
/**
* Sets a {@link GrantedAuthoritiesMapper} to use for mapping
* {@link GrantedAuthority}s, defaults to no-op implementation.
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} to use
*/
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
this.authoritiesMapper = authoritiesMapper;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OidcIdToken#getIssuedAt()} to match the existing
* {@link OidcUser#getIdToken()}'s issuedAt time, defaults to 60 seconds.
* @param clockSkew the maximum acceptable clock skew to use
*/
public void setClockSkew(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
this.clockSkew = clockSkew;
}
private String extractIdToken(Map<String, Object> attributes) {
if (attributes.get(OidcParameterNames.ID_TOKEN) instanceof String idToken) {
return idToken;
}
return null;
}
private void validateIdToken(OidcUser existingOidcUser, OidcIdToken idToken) {
// OpenID Connect Core 1.0 - Section 12.2 Successful Refresh Response
// If an ID Token is returned as a result of a token refresh request, the
// following requirements apply:
// its iss Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateIssuer(existingOidcUser, idToken);
// its sub Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateSubject(existingOidcUser, idToken);
// its iat Claim MUST represent the time that the new ID Token is issued,
validateIssuedAt(existingOidcUser, idToken);
// its aud Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateAudience(existingOidcUser, idToken);
// if the ID Token contains an auth_time Claim, its value MUST represent the time
// of the original authentication - not the time that the new ID token is issued,
validateAuthenticatedAt(existingOidcUser, idToken);
// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of
// the original authentication contained nonce; however, if it is present, its
// value MUST be the same as in the ID Token issued at the time of the original
// authentication,
validateNonce(existingOidcUser, idToken);
}
private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getSubject().equals(existingOidcUser.getIdToken().getSubject())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getIssuedAt().isAfter(existingOidcUser.getIdToken().getIssuedAt().minus(this.clockSkew))) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!isValidAudience(existingOidcUser, idToken)) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid audience",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private boolean isValidAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
List<String> idTokenAudiences = idToken.getAudience();
Set<String> oidcUserAudiences = new HashSet<>(existingOidcUser.getIdToken().getAudience());
if (idTokenAudiences.size() != oidcUserAudiences.size()) {
return false;
}
for (String audience : idTokenAudiences) {
if (!oidcUserAudiences.contains(audience)) {
return false;
}
}
return true;
}
private void validateAuthenticatedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
if (idToken.getAuthenticatedAt() == null) {
return;
}
if (!idToken.getAuthenticatedAt().equals(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());
}
}
private void validateNonce(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!StringUtils.hasText(idToken.getNonce())) {
return;
}
if (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, "Invalid nonce",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private Mono<Void> refreshSecurityContext(ServerWebExchange exchange, ClientRegistration clientRegistration,
OAuth2AuthenticationToken authenticationToken, OidcUser oidcUser) {
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oidcUser.getAuthorities());
OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(oidcUser, mappedAuthorities,
clientRegistration.getRegistrationId());
authenticationResult.setDetails(authenticationToken.getDetails());
SecurityContext securityContext = new SecurityContextImpl(authenticationResult);
return this.serverSecurityContextRepository.save(exchange, securityContext);
}
}
@@ -21,9 +21,7 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import reactor.core.publisher.Mono;
@@ -42,7 +40,6 @@ import org.springframework.util.Assert;
* {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
*
* @author Joe Grandja
* @author Evgeniy Cheban
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
* @see WebClientReactiveRefreshTokenTokenResponseClient
@@ -52,8 +49,6 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = new WebClientReactiveRefreshTokenTokenResponseClient();
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
@@ -101,17 +96,12 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
.flatMap(this.accessTokenResponseClient::getTokenResponse)
.onErrorMap(OAuth2AuthorizationException.class,
(e) -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(), e))
.flatMap((tokenResponse) -> {
OAuth2AuthorizedClient refreshedClient = new OAuth2AuthorizedClient(clientRegistration,
context.getPrincipal().getName(), tokenResponse.getAccessToken(),
tokenResponse.getRefreshToken());
Map<String, Object> attributes = new HashMap<>(context.getAttributes());
attributes.putAll(tokenResponse.getAdditionalParameters());
return this.authorizationSuccessHandler
.onAuthorizationSuccess(refreshedClient, context.getPrincipal(),
Collections.unmodifiableMap(attributes))
.thenReturn(refreshedClient);
});
.map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
}
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
/**
@@ -126,19 +116,6 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets a {@link ReactiveOAuth2AuthorizationSuccessHandler} to use for handling
* successful refresh token response, defaults to
* {@link RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler}.
* @param authorizationSuccessHandler the
* {@link ReactiveOAuth2AuthorizationSuccessHandler} to use
* @since 7.1
*/
public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
this.authorizationSuccessHandler = authorizationSuccessHandler;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
@@ -166,8 +143,4 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
this.clock = clock;
}
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
}
@@ -16,13 +16,8 @@
package org.springframework.security.oauth2.client.aot.hint;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.util.ClassUtils;
/**
* {@link RuntimeHintsRegistrar} for OAuth2 Client
@@ -32,20 +27,9 @@ import org.springframework.util.ClassUtils;
*/
class OAuth2ClientRuntimeHints implements RuntimeHintsRegistrar {
private static final boolean r2dbcPresent;
static {
ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
r2dbcPresent = ClassUtils.isPresent("io.r2dbc.spi.Row", classLoader)
&& ClassUtils.isPresent("org.springframework.r2dbc.core.DatabaseClient", classLoader);
}
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
registerOAuth2ClientSchemaFilesHints(hints);
if (r2dbcPresent) {
registerR2dbcHints(hints);
}
}
private void registerOAuth2ClientSchemaFilesHints(RuntimeHints hints) {
@@ -54,23 +38,4 @@ class OAuth2ClientRuntimeHints implements RuntimeHintsRegistrar {
.registerPattern("org/springframework/security/oauth2/client/oauth2-client-schema-postgres.sql");
}
private void registerR2dbcHints(RuntimeHints hints) {
// Register R2DBC OAuth2 client service types
hints.reflection()
.registerType(
TypeReference
.of("org.springframework.security.oauth2.client.R2dbcReactiveOAuth2AuthorizedClientService"),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS));
// Register OAuth2 client types that may be serialized in R2DBC scenarios
hints.reflection()
.registerTypes(java.util.List.of(
TypeReference.of("org.springframework.security.oauth2.client.OAuth2AuthorizedClient"),
TypeReference
.of("org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken")),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS));
}
}
@@ -105,7 +105,7 @@ public final class ClientRegistrations {
* @return the {@link ClientRegistration} built from the configuration
*/
public static ClientRegistration.Builder fromOidcConfiguration(Map<String, Object> configuration) {
OIDCProviderMetadata metadata = parseInput(configuration, OIDCProviderMetadata::parse);
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
ClientRegistration.Builder builder = withProviderConfiguration(metadata, metadata.getIssuer().getValue());
builder.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
if (metadata.getUserInfoEndpointURI() != null) {
@@ -292,15 +292,6 @@ public final class ClientRegistrations {
throw new IllegalArgumentException(errorMessage);
}
private static <T> T parseInput(Map<String, Object> body, ThrowingFunction<JSONObject, T, ParseException> parser) {
try {
return parse(body, parser);
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(ex);
}
}
private static <T> T parse(Map<String, Object> body, ThrowingFunction<JSONObject, T, ParseException> parser) {
try {
return parser.apply(new JSONObject(body));
@@ -318,14 +318,14 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React
return Mono.justOrEmpty(serverWebExchange)
.switchIfEmpty(currentServerWebExchangeMono)
.flatMap((exchange) -> {
Map<String, Object> contextAttributes = new HashMap<>();
contextAttributes.put(ServerWebExchange.class.getName(), serverWebExchange);
Map<String, Object> contextAttributes = Collections.emptyMap();
String scope = exchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope)) {
contextAttributes = new HashMap<>();
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
StringUtils.delimitedListToStringArray(scope, " "));
}
return Mono.just(Collections.unmodifiableMap(contextAttributes));
return Mono.just(contextAttributes);
})
.defaultIfEmpty(Collections.emptyMap());
// @formatter:on
@@ -51,8 +51,6 @@ import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
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.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.ClientRequest;
@@ -98,7 +96,6 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rob Winch
* @author Joe Grandja
* @author Phil Clay
* @author Evgeniy Cheban
* @since 5.1
*/
public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
@@ -142,8 +139,6 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
private ClientResponseHandler clientResponseHandler;
private ServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
/**
* Constructs a {@code ServerOAuth2AuthorizedClientExchangeFilterFunction} using the
* provided parameters.
@@ -335,11 +330,8 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
}
private Mono<ClientResponse> exchangeAndHandleResponse(ClientRequest request, ExchangeFunction next) {
// Re-request an Authentication from serverSecurityContextRepository since it
// might have been changed during provider invocation.
return effectiveAuthentication(request).flatMap((authentication) -> next.exchange(request)
.transform((responseMono) -> this.clientResponseHandler.handleResponse(request, responseMono))
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)));
return next.exchange(request)
.transform((responseMono) -> this.clientResponseHandler.handleResponse(request, responseMono));
}
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request) {
@@ -370,17 +362,6 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
// @formatter:on
}
private Mono<Authentication> effectiveAuthentication(ClientRequest request) {
// @formatter:off
return effectiveServerWebExchange(request)
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(this.serverSecurityContextRepository::load)
.mapNotNull(SecurityContext::getAuthentication)
.switchIfEmpty(this.currentAuthenticationMono);
// @formatter:on
}
/**
* Returns a {@link Mono} the emits the {@code clientRegistrationId} that is active
* for the given request.
@@ -464,19 +445,6 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler);
}
/**
* Sets a {@link ServerSecurityContextRepository} to use for re-obtaining a
* {@link SecurityContext} if it has been refreshed during provider invocation,
* defaults to {@link WebSessionServerSecurityContextRepository}.
* @param serverSecurityContextRepository the {@link ServerSecurityContextRepository}
* to use
* @since 7.1
*/
public void setServerSecurityContextRepository(ServerSecurityContextRepository serverSecurityContextRepository) {
Assert.notNull(serverSecurityContextRepository, "serverSecurityContextRepository cannot be null");
this.serverSecurityContextRepository = serverSecurityContextRepository;
}
@FunctionalInterface
private interface ClientResponseHandler {
@@ -1,398 +0,0 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler}.
*
* @author Evgeniy Cheban
*/
class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests {
@Test
void setServerSecurityContextRepositoryWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler()
.setServerSecurityContextRepository(null))
.withMessage("serverSecurityContextRepository cannot be null");
}
@Test
void setJwtDecoderFactoryWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setJwtDecoderFactory(null))
.withMessage("jwtDecoderFactory cannot be null");
}
@Test
void setAuthoritiesMapperWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setAuthoritiesMapper(null))
.withMessage("authoritiesMapper cannot be null");
}
@Test
void setUserServiceWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setUserService(null))
.withMessage("userService cannot be null");
}
@Test
void setClockSkewWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setClockSkew(null))
.withMessage("clockSkew cannot be null");
}
@Test
void onAuthorizationSuccessWhenIdTokenValidThenSecurityContextRefreshed() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyComplete();
StepVerifier.create(serverSecurityContextRepository.load(exchange).map(SecurityContext::getAuthentication))
.expectNext(authenticationToken)
.verifyComplete();
}
@Test
void onAuthorizationSuccessWhenIdTokenIssuerNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", "https://issuer.com");
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid issuer");
}
@Test
void onAuthorizationSuccessWhenIdTokenSubNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", "invalid_sub");
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid subject");
}
@Test
void onAuthorizationSuccessWhenIdTokenIatNotAfterThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt().minus(Duration.ofDays(1)));
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid issued at time");
}
@Test
void onAuthorizationSuccessWhenIdTokenAudEmptyThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", Collections.emptyList());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid audience");
}
@Test
void onAuthorizationSuccessWhenIdTokenAudNotContainThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", List.of("invalid_client-id"));
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid audience");
}
@Test
void onAuthorizationSuccessWhenIdTokenAuthTimeNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("auth_time", principal.getIssuedAt());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid authenticated at time");
}
@Test
void onAuthorizationSuccessWhenIdTokenNonceNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", "invalid_nonce");
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_nonce] Invalid nonce");
}
private static OAuth2AccessToken createAccessToken() {
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token-1234", issuedAt, expiresAt,
Set.of(OidcScopes.OPENID));
}
}

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