Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9d4dc8fc | |||
| 3d4e20597a | |||
| 81bd52ae48 | |||
| 25b6af2738 | |||
| 95987bffc1 | |||
| 6e5f8f2a1d | |||
| 4187af38b2 | |||
| 5b638a54a4 | |||
| 51eef2b980 | |||
| 302cfb116e | |||
| 695ea1717f | |||
| 1206c2b141 | |||
| 3539f06146 | |||
| a317a3d866 | |||
| 68b820ed09 | |||
| 44d32815b1 | |||
| 87c3335e01 | |||
| eefbb4da64 | |||
| a2793f31b4 | |||
| 679a47a51d | |||
| 08fca57d12 | |||
| acabacb971 | |||
| 1a130fca3c | |||
| 5a4ada04ac | |||
| a856baa6a8 | |||
| ac63cf4fa5 | |||
| f6bb55effb | |||
| 85b756cb74 | |||
| 7441ce7f16 | |||
| 9dbcd8cf00 | |||
| 835d6c1fbd | |||
| 95b2cdf7f4 | |||
| 3ecf84855e | |||
| 0039bc0cf0 | |||
| 057e5181ea | |||
| 178ca56aaf | |||
| 61ccf14953 | |||
| 6e683f2286 | |||
| b6e24db68c | |||
| aeb5fc1fb0 | |||
| 62f33d3fcf | |||
| 9fed1ac8c3 | |||
| 9dbe3bdcc0 | |||
| d547ae0181 | |||
| b8b1278e1f | |||
| 381047e386 | |||
| 376b40a735 | |||
| 89fa1cbdd2 | |||
| 0d75e6d10c | |||
| 01758c4c59 | |||
| f37833a59c | |||
| 52e6c4c4be | |||
| 96ceb535f4 | |||
| 0c54a55ae8 |
@@ -14,4 +14,4 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
|
||||
|
||||
@@ -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@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: inherit
|
||||
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@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
@@ -67,21 +67,21 @@ jobs:
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build, test, check-samples ]
|
||||
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@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build, test, check-samples ]
|
||||
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@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
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@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
|
||||
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
|
||||
@@ -97,6 +97,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -18,21 +18,21 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Compute Version
|
||||
id: compute-version
|
||||
uses: spring-io/spring-release-actions/compute-version@0.0.3
|
||||
uses: spring-io/spring-release-actions/compute-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
- name: Get Today's Release Version
|
||||
id: todays-release
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@0.0.3
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
||||
milestone-repository: ${{ github.repository }}
|
||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute Next Version
|
||||
id: next-version
|
||||
uses: spring-io/spring-release-actions/compute-next-version@0.0.3
|
||||
uses: spring-io/spring-release-actions/compute-next-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.todays-release.outputs.release-version }}
|
||||
- name: Schedule Next Milestone
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@0.0.3
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.next-version.outputs.version }}
|
||||
version-date: ${{ steps.next-version.outputs.version-date }}
|
||||
|
||||
@@ -30,6 +30,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
@@ -46,6 +46,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
matrix:
|
||||
branch: [ '5.8.x', '6.2.x', '6.3.x', 'main' ]
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: ${{ matrix.branch }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on docs-build
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: 'docs-build'
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
jobs:
|
||||
update-scheduled-release-version:
|
||||
name: Update Scheduled Release Version
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
+3
@@ -36,6 +36,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
|
||||
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
|
||||
import org.springframework.security.web.webauthn.management.CredentialRecordOwnerAuthorizationManager;
|
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
@@ -166,6 +167,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
|
||||
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
|
||||
rpOperations);
|
||||
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
|
||||
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
|
||||
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
|
||||
rpOperations);
|
||||
if (creationOptionsRepository != null) {
|
||||
|
||||
@@ -211,6 +211,7 @@ import org.springframework.security.web.webauthn.api.AuthenticationExtensionsCli
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
|
||||
@@ -225,6 +226,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestO
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestBytes;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
|
||||
@@ -654,6 +656,8 @@ final class SerializationSamples {
|
||||
generatorByClassName.put(CredentialPropertiesOutput.class, (o) -> credentialOutput);
|
||||
generatorByClassName.put(ImmutableAuthenticationExtensionsClientOutputs.class, (o) -> outputs);
|
||||
generatorByClassName.put(AuthenticatorAssertionResponse.class, (r) -> response);
|
||||
generatorByClassName.put(AuthenticatorAttestationResponse.class,
|
||||
(r) -> TestAuthenticatorAttestationResponses.createAuthenticatorAttestationResponse().build());
|
||||
generatorByClassName.put(RelyingPartyAuthenticationRequest.class, (r) -> authRequest);
|
||||
generatorByClassName.put(PublicKeyCredential.class, (r) -> credential);
|
||||
generatorByClassName.put(WebAuthnAuthenticationRequestToken.class, (r) -> requestToken);
|
||||
|
||||
+54
-5
@@ -33,10 +33,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
@@ -207,10 +207,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
boolean hasSerialVersion = Stream.of(clazz.getDeclaredFields())
|
||||
.map(Field::getName)
|
||||
.anyMatch((n) -> n.equals("serialVersionUID"));
|
||||
SuppressWarnings suppressWarnings = clazz.getAnnotation(SuppressWarnings.class);
|
||||
boolean hasSerialIgnore = suppressWarnings == null
|
||||
|| Arrays.asList(suppressWarnings.value()).contains("Serial");
|
||||
if (!hasSerialVersion && !hasSerialIgnore) {
|
||||
if (!hasSerialVersion && !hasSuppressSerialInSource(clazz)) {
|
||||
classes.add(clazz);
|
||||
continue;
|
||||
}
|
||||
@@ -249,6 +246,58 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
return classes.stream();
|
||||
}
|
||||
|
||||
private static boolean hasSuppressSerialInSource(Class<?> clazz) {
|
||||
try {
|
||||
Class<?> fileClass = clazz;
|
||||
while (fileClass.getEnclosingClass() != null) {
|
||||
fileClass = fileClass.getEnclosingClass();
|
||||
}
|
||||
var codeSource = fileClass.getProtectionDomain().getCodeSource();
|
||||
if (codeSource == null) {
|
||||
return false;
|
||||
}
|
||||
Path sourceFile = findSourceFile(Path.of(codeSource.getLocation().toURI()), fileClass);
|
||||
if (sourceFile == null) {
|
||||
return false;
|
||||
}
|
||||
return hasSuppressSerialAnnotation(Files.readAllLines(sourceFile), clazz.getSimpleName());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path findSourceFile(Path start, Class<?> clazz) {
|
||||
String relativePath = clazz.getName().replace('.', '/') + ".java";
|
||||
Path dir = start;
|
||||
for (int i = 0; i < 10 && dir != null; i++) {
|
||||
for (String sourceRoot : List.of("src/main/java", "src/test/java")) {
|
||||
Path candidate = dir.resolve(sourceRoot).resolve(relativePath);
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
dir = dir.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean hasSuppressSerialAnnotation(List<String> lines, String simpleClassName) {
|
||||
Pattern classDeclaration = Pattern
|
||||
.compile("\\b(?:class|interface|enum|record)\\s+" + Pattern.quote(simpleClassName) + "\\b");
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (classDeclaration.matcher(lines.get(i)).find()) {
|
||||
for (int j = Math.max(0, i - 5); j < i; j++) {
|
||||
String line = lines.get(j);
|
||||
if (line.contains("@SuppressWarnings") && line.contains("\"serial\"")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getCurrentVersion() {
|
||||
String version = System.getProperty("springSecurityVersion");
|
||||
String[] parts = version.split("\\.");
|
||||
|
||||
+69
@@ -42,8 +42,15 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
@@ -56,6 +63,7 @@ import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
@@ -247,6 +255,24 @@ public class WebAuthnConfigurerTests {
|
||||
.andExpect(content().string(expectedBody));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
|
||||
this.spring.register(DeleteCredentialConfiguration.class).autowire();
|
||||
this.mvc
|
||||
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
|
||||
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
|
||||
this.spring.register(DeleteCredentialConfiguration.class).autowire();
|
||||
this.mvc
|
||||
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
|
||||
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class ConfigCredentialCreationOptionsRepository {
|
||||
@@ -417,4 +443,47 @@ public class WebAuthnConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class DeleteCredentialConfiguration {
|
||||
|
||||
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
|
||||
|
||||
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
|
||||
return mock(WebAuthnRelyingPartyOperations.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserCredentialRepository userCredentialRepository() {
|
||||
MapUserCredentialRepository repository = new MapUserCredentialRepository();
|
||||
repository.save(TestCredentialRecords.userCredential().build());
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PublicKeyCredentialUserEntityRepository userEntityRepository() {
|
||||
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
|
||||
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(USER_ENTITY_ID)
|
||||
.displayName("User")
|
||||
.build());
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+25
@@ -28,14 +28,17 @@ import jakarta.servlet.http.HttpSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.session.SessionLimit;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -150,6 +153,16 @@ public class HttpHeadersConfigTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenHeadersEagerlyConfiguredThenHeadersAreWritten() throws Exception {
|
||||
this.spring.configLocations(this.xml("HeadersEagerlyConfigured")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/").secure(true))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(includesDefaults());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenFrameOptionsConfiguredThenIncludesHeader() throws Exception {
|
||||
Map<String, String> headers = new HashMap(defaultHeaders);
|
||||
@@ -955,6 +968,18 @@ public class HttpHeadersConfigTests {
|
||||
|
||||
}
|
||||
|
||||
public static class EagerHeadersBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof HeaderWriterFilter headerWriterFilter) {
|
||||
headerWriterFilter.setShouldWriteHeadersEagerly(true);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomSessionLimit implements SessionLimit {
|
||||
|
||||
@Override
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http auto-config="true">
|
||||
<headers/>
|
||||
<intercept-url pattern="/**" access="permitAll"/>
|
||||
</http>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.HttpHeadersConfigTests.EagerHeadersBeanPostProcessor"/>
|
||||
|
||||
<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
BIN
Binary file not shown.
+39
-4
@@ -92,6 +92,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
|
||||
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
|
||||
|
||||
private boolean alwaysPerformAdditionalChecksOnUser = true;
|
||||
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
|
||||
|
||||
/**
|
||||
@@ -146,8 +148,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
|
||||
}
|
||||
try {
|
||||
this.preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
if (!cacheWasUsed) {
|
||||
@@ -157,8 +158,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
// we're using latest data (i.e. not from the cache)
|
||||
cacheWasUsed = false;
|
||||
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
|
||||
this.preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
}
|
||||
this.postAuthenticationChecks.check(user);
|
||||
if (!cacheWasUsed) {
|
||||
@@ -171,6 +171,25 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
return createSuccessAuthentication(principalToReturn, authentication, user);
|
||||
}
|
||||
|
||||
private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) {
|
||||
try {
|
||||
this.preAuthenticationChecks.check(user);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
if (!this.alwaysPerformAdditionalChecksOnUser) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
additionalAuthenticationChecks(user, authentication);
|
||||
}
|
||||
catch (AuthenticationException ignored) {
|
||||
// preserve the original failed check
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
additionalAuthenticationChecks(user, authentication);
|
||||
}
|
||||
|
||||
private String determineUsername(Authentication authentication) {
|
||||
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
|
||||
}
|
||||
@@ -313,6 +332,22 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
this.postAuthenticationChecks = postAuthenticationChecks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to always perform the additional checks on the user, even if the
|
||||
* pre-authentication checks fail. This is useful to ensure that regardless of the
|
||||
* state of the user account, authentication takes the same amount of time to
|
||||
* complete.
|
||||
*
|
||||
* <p>
|
||||
* For applications that rely on the additional checks running only once should set
|
||||
* this value to {@code false}
|
||||
* @param alwaysPerformAdditionalChecksOnUser
|
||||
* @since 5.7.23
|
||||
*/
|
||||
public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) {
|
||||
this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser;
|
||||
}
|
||||
|
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||
this.authoritiesMapper = authoritiesMapper;
|
||||
}
|
||||
|
||||
+5
-3
@@ -152,7 +152,9 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
||||
return null;
|
||||
}
|
||||
OneTimeToken token = tokens.get(0);
|
||||
deleteOneTimeToken(token);
|
||||
if (deleteOneTimeToken(token) == 0) {
|
||||
return null;
|
||||
}
|
||||
if (isExpired(token)) {
|
||||
return null;
|
||||
}
|
||||
@@ -170,11 +172,11 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
||||
return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper);
|
||||
}
|
||||
|
||||
private void deleteOneTimeToken(OneTimeToken oneTimeToken) {
|
||||
private int deleteOneTimeToken(OneTimeToken oneTimeToken) {
|
||||
List<SqlParameterValue> parameters = List
|
||||
.of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
|
||||
return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
|
||||
}
|
||||
|
||||
private ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||
* instead
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
public class ExpressionAttributeAuthorizationDecision extends AuthorizationDecision {
|
||||
|
||||
private final ExpressionAttribute expressionAttribute;
|
||||
|
||||
+42
-6
@@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
@@ -42,6 +43,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
@@ -62,6 +64,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -452,12 +455,10 @@ public class DaoAuthenticationProviderTests {
|
||||
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
|
||||
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
|
||||
* SEC-2056 is fixed.
|
||||
*/
|
||||
public void IGNOREtestSec2056() {
|
||||
// SEC-2056
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
|
||||
public void testSec2056() {
|
||||
UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod",
|
||||
"koala");
|
||||
UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken
|
||||
@@ -491,6 +492,41 @@ public class DaoAuthenticationProviderTests {
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
// related to SEC-2056
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
|
||||
public void testDisabledUserTiming() {
|
||||
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
|
||||
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setPasswordEncoder(encoder);
|
||||
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
|
||||
users.password = encoder.encode((CharSequence) user.getCredentials());
|
||||
provider.setUserDetailsService(users);
|
||||
int sampleSize = 100;
|
||||
List<Long> enabledTimes = new ArrayList<>(sampleSize);
|
||||
for (int i = 0; i < sampleSize; i++) {
|
||||
long start = System.currentTimeMillis();
|
||||
provider.authenticate(user);
|
||||
enabledTimes.add(System.currentTimeMillis() - start);
|
||||
}
|
||||
UserDetailsChecker preChecks = mock(UserDetailsChecker.class);
|
||||
willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class));
|
||||
provider.setPreAuthenticationChecks(preChecks);
|
||||
List<Long> disabledTimes = new ArrayList<>(sampleSize);
|
||||
for (int i = 0; i < sampleSize; i++) {
|
||||
long start = System.currentTimeMillis();
|
||||
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user));
|
||||
disabledTimes.add(System.currentTimeMillis() - start);
|
||||
}
|
||||
double enabledAvg = avg(enabledTimes);
|
||||
double disabledAvg = avg(disabledTimes);
|
||||
assertThat(Math.abs(disabledAvg - enabledAvg) <= 3)
|
||||
.withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average "
|
||||
+ enabledAvg)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
private double avg(List<Long> counts) {
|
||||
return counts.stream().mapToLong(Long::longValue).average().orElse(0);
|
||||
}
|
||||
|
||||
+26
@@ -21,19 +21,24 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.PreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -145,6 +150,27 @@ class JdbcOneTimeTokenServiceTests {
|
||||
assertThat(consumedOneTimeToken).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception {
|
||||
// Simulates a concurrent consume: SELECT finds the token but DELETE affects
|
||||
// 0 rows because another caller already consumed it.
|
||||
JdbcOperations jdbcOperations = mock(JdbcOperations.class);
|
||||
Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES);
|
||||
OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired);
|
||||
given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class),
|
||||
ArgumentMatchers.<RowMapper<OneTimeToken>>any()))
|
||||
.willReturn(List.of(token));
|
||||
given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0);
|
||||
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations);
|
||||
try {
|
||||
OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE));
|
||||
assertThat(consumed).isNull();
|
||||
}
|
||||
finally {
|
||||
service.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeWhenTokenIsExpiredThenReturnNull() {
|
||||
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
|
||||
|
||||
@@ -376,7 +376,9 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -386,7 +388,9 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -452,8 +456,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -463,8 +469,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -479,8 +487,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -490,8 +500,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -506,11 +518,13 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(ES512);
|
||||
}).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -520,12 +534,14 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms {
|
||||
it.add(RS512)
|
||||
it.add(ES512)
|
||||
}
|
||||
.build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
Once you have got an application that is xref:servlet/authentication/index.adoc[authenticating requests], it is important to consider how that resulting authentication will be persisted and restored on future requests.
|
||||
|
||||
This is done automatically by default, so no additional code is necessary, though it is important to know what `requireExplicitSave` means in `HttpSecurity`.
|
||||
This is done automatically by default.
|
||||
If you have a custom filter or controller that is setting the security context, you will need to use a `SecurityContextRepository` to persist it across requests.
|
||||
|
||||
If you are upgrading from an older version, you may be interested in the `requireExplicitSave` setting that preserves Spring Security 5's default, though note that this is primarily for migration purposes.
|
||||
|
||||
If you like, <<how-it-works-requireexplicitsave,you can read more about what requireExplicitSave is doing>> or <<requireexplicitsave,why it's important>>. Otherwise, in most cases you are done with this section.
|
||||
|
||||
|
||||
@@ -1377,14 +1377,17 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
|
||||
public class MyPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
|
||||
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MyPostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
|
||||
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
@@ -1395,12 +1398,15 @@ Kotlin::
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
|
||||
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
|
||||
class MyPreAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocation> {
|
||||
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
|
||||
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
|
||||
@Component
|
||||
class MyPostAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocationResult> {
|
||||
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
@@ -1422,13 +1428,13 @@ Java::
|
||||
class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preAuthorize(MyAuthorizationManager manager) {
|
||||
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postAuthorize(MyAuthorizationManager manager) {
|
||||
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
|
||||
}
|
||||
}
|
||||
@@ -1441,15 +1447,15 @@ Kotlin::
|
||||
@Configuration
|
||||
@EnableMethodSecurity(prePostEnabled = false)
|
||||
class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun preAuthorize(manager: MyAuthorizationManager) : Advisor {
|
||||
fun preAuthorize(manager: MyPreAuthorizeAuthorizationManager): Advisor {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun postAuthorize(manager: MyAuthorizationManager) : Advisor {
|
||||
fun postAuthorize(manager: MyPostAuthorizeAuthorizationManager): Advisor {
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
|
||||
}
|
||||
}
|
||||
@@ -1466,13 +1472,13 @@ Xml::
|
||||
<bean id="preAuthorize"
|
||||
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
|
||||
factory-method="preAuthorize">
|
||||
<constructor-arg ref="myAuthorizationManager"/>
|
||||
<constructor-arg ref="myPreAuthorizeAuthorizationManager"/>
|
||||
</bean>
|
||||
|
||||
<bean id="postAuthorize"
|
||||
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
|
||||
factory-method="postAuthorize">
|
||||
<constructor-arg ref="myAuthorizationManager"/>
|
||||
<constructor-arg ref="myPostAuthorizeAuthorizationManager"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
@@ -1482,6 +1488,8 @@ Xml::
|
||||
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
|
||||
====
|
||||
|
||||
You can also implement `MethodAuthorizationDeniedHandler` in the same manager class to override the default exception-handling behavior.
|
||||
|
||||
[[customizing-expression-handling]]
|
||||
=== Customizing Expression Handling
|
||||
|
||||
|
||||
@@ -519,7 +519,9 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -529,7 +531,9 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -595,8 +599,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -606,8 +612,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -622,8 +630,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -633,8 +643,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -649,11 +661,13 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(ES512);
|
||||
}).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -663,11 +677,13 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms {
|
||||
it.add(RS512)
|
||||
it.add(ES512)
|
||||
}.build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@antora/collector-extension": "1.0.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
"@springio/antora-extensions": "1.14.11",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=3.3.3
|
||||
version=6.5.9-SNAPSHOT
|
||||
version=6.5.10
|
||||
samplesBranch=6.5.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -14,7 +14,7 @@ org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-opensaml5 = "5.1.2"
|
||||
org-springframework = "6.2.17"
|
||||
org-springframework = "6.2.18"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
|
||||
@@ -31,13 +31,13 @@ commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.4"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.14"
|
||||
io-mockk = "io.mockk:mockk:1.14.7"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.16"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.17"
|
||||
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.15"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
@@ -57,7 +57,7 @@ org-apache-directory-server-apacheds-protocol-shared = { module = "org.apache.di
|
||||
org-apache-directory-server-apacheds-server-jndi = { module = "org.apache.directory.server:apacheds-server-jndi", version.ref = "org-apache-directory-server" }
|
||||
org-apache-directory-shared-shared-ldap = "org.apache.directory.shared:shared-ldap:0.9.15"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents:httpclient:4.5.14"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.14"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.15"
|
||||
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
|
||||
@@ -71,7 +71,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
|
||||
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
|
||||
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.44.Final"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.49.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 = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
|
||||
|
||||
+5
-1
@@ -300,7 +300,11 @@ public final class OidcAuthorizedClientRefreshedEventListener
|
||||
return;
|
||||
}
|
||||
|
||||
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
|
||||
// The auth_time claim MUST represent the time of the original authentication OR
|
||||
// the most recent time when the end-user reauthenticated when "prompt=login" is
|
||||
// passed in the authentication request
|
||||
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
|
||||
&& !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
|
||||
REFRESH_TOKEN_RESPONSE_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
|
||||
+1
@@ -269,6 +269,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class OAuth2AuthorizationRequestException extends AuthenticationException {
|
||||
|
||||
OAuth2AuthorizationRequestException(Throwable cause) {
|
||||
|
||||
+21
-3
@@ -419,8 +419,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtDoesNotMatchThenThrowsOAuth2AuthenticationException() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.SECONDS);
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtBeforeCurrentAuthenticatedAtThenThrowsOAuth2AuthenticationException() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().minus(5, ChronoUnit.MINUTES);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
|
||||
@@ -440,8 +440,26 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
|
||||
verifyNoInteractions(this.userService, this.applicationEventPublisher);
|
||||
}
|
||||
|
||||
// gh-18839
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesThenOidcUserRefreshedEventPublished() {
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtAfterCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.MINUTES);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
|
||||
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
|
||||
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
|
||||
|
||||
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
|
||||
this.accessTokenResponse, this.authorizedClient);
|
||||
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
|
||||
|
||||
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
|
||||
verifyNoMoreInteractions(this.applicationEventPublisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
|
||||
Instant authTime = this.authentication.getPrincipal().getAttribute(IdTokenClaimNames.AUTH_TIME);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
|
||||
+35
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.oidc.user;
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
@@ -114,4 +115,38 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
||||
return this.userInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DefaultOidcUser that = (DefaultOidcUser) obj;
|
||||
if (!this.getName().equals(that.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (!this.getAuthorities().equals(that.getAuthorities())) {
|
||||
return false;
|
||||
}
|
||||
if (this.getIdToken().getIssuer() == null || that.getIdToken().getIssuer() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(this.getIdToken().getIssuer().toExternalForm(),
|
||||
that.getIdToken().getIssuer().toExternalForm())
|
||||
&& Objects.equals(this.getIdToken().getSubject(), that.getIdToken().getSubject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.getName().hashCode();
|
||||
result = 31 * result + this.getAuthorities().hashCode();
|
||||
result = 31 * result + ((this.getIdToken().getIssuer() != null)
|
||||
? this.getIdToken().getIssuer().toExternalForm().hashCode() : 0);
|
||||
result = 31 * result
|
||||
+ ((this.getIdToken().getSubject() != null) ? this.getIdToken().getSubject().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.core.oidc.user;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -147,4 +148,31 @@ public class DefaultOidcUserTests {
|
||||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
// gh-18622
|
||||
@Test
|
||||
public void equalsWhenOidcUserPrincipalSameThenTrue() {
|
||||
String issuer = "https://example.com";
|
||||
String subject = "subject-1";
|
||||
|
||||
// @formatter:off
|
||||
OidcIdToken idToken1 = OidcIdToken.withTokenValue("id-token-value-1")
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
|
||||
.build();
|
||||
|
||||
OidcIdToken idToken2 = OidcIdToken.withTokenValue("id-token-value-2")
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
DefaultOidcUser user1 = new DefaultOidcUser(AUTHORITIES, idToken1, USER_INFO);
|
||||
DefaultOidcUser user2 = new DefaultOidcUser(AUTHORITIES, idToken2, USER_INFO);
|
||||
assertThat(user1).isEqualTo(user2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -185,6 +185,7 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPP
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class JtiCache extends LinkedHashMap<String, Long> {
|
||||
|
||||
private static final int MAX_SIZE = 1000;
|
||||
|
||||
+13
-2
@@ -230,7 +230,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
.getConfigurationForIssuerLocation(issuer, rest);
|
||||
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
|
||||
return configuration.get("jwks_uri").toString();
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,6 +290,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
|
||||
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (rest) -> jwkSetUri;
|
||||
@@ -423,6 +426,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
Assert.notNull(validator, "validator cannot be null");
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
|
||||
if (this.signatureAlgorithms.isEmpty()) {
|
||||
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
|
||||
@@ -461,7 +470,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
* @return the configured {@link NimbusJwtDecoder}
|
||||
*/
|
||||
public NimbusJwtDecoder build() {
|
||||
return new NimbusJwtDecoder(processor());
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
|
||||
|
||||
+12
-2
@@ -241,7 +241,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
}
|
||||
return Mono.just(configuration.get("jwks_uri").toString());
|
||||
}),
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,6 +333,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
|
||||
private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
|
||||
@@ -456,6 +459,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(
|
||||
BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) {
|
||||
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
|
||||
@@ -468,7 +476,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
* @return the configured {@link NimbusReactiveJwtDecoder}
|
||||
*/
|
||||
public NimbusReactiveJwtDecoder build() {
|
||||
return new NimbusReactiveJwtDecoder(processor());
|
||||
NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
|
||||
|
||||
+16
-1
@@ -328,11 +328,26 @@ public class NimbusJwtDecoderTests {
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
|
||||
.restOperations(restOperations)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class)))
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
|
||||
// @formatter:off
|
||||
|
||||
+22
-2
@@ -617,11 +617,31 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
WebClient real = WebClient.builder().build();
|
||||
WebClient.RequestHeadersUriSpec spec = spy(real.get());
|
||||
WebClient webClient = spy(WebClient.class);
|
||||
given(webClient.get()).willReturn(spec);
|
||||
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
|
||||
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
assertThatExceptionOfType(JwtValidationException.class)
|
||||
.isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -30,6 +30,23 @@ import org.springframework.util.Assert;
|
||||
* A {@link Jwt} to {@link GrantedAuthority} {@link Converter} that is a composite of
|
||||
* converters.
|
||||
*
|
||||
* <p>
|
||||
* This is handy when needing to read authorities from multiple locations in a JWT; each
|
||||
* underlying converter is called in series and the results are aggregated into a single
|
||||
* collection of authorities.
|
||||
*
|
||||
* <p>
|
||||
* For example, you might have a claim called "scope" and another called "roles". With
|
||||
* {@link DelegatingJwtGrantedAuthoritiesConverter}, you can do:
|
||||
*
|
||||
* <code>
|
||||
* JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
|
||||
* JwtGrantedAuthoritiesConverter roles = new JwtGrantedAUthoritiesConverter();
|
||||
* roles.setAuthoritiesClaimName("roles");
|
||||
* roles.setAuthorityPrefix("ROLE_");
|
||||
* return new DelegatingJwtGrantedAuthoritiesConverter(scopes, roles);
|
||||
* </code>
|
||||
*
|
||||
* @author Laszlo Stahorszki
|
||||
* @author Josh Cummings
|
||||
* @since 5.5
|
||||
|
||||
-3
@@ -36,9 +36,6 @@ import org.springframework.util.Assert;
|
||||
* Uses an expression for extracting the token claim value to use for mapping
|
||||
* {@link GrantedAuthority authorities}.
|
||||
*
|
||||
* Note this can be used in combination with a
|
||||
* {@link DelegatingJwtGrantedAuthoritiesConverter}.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @since 6.4
|
||||
*/
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
@@ -33,6 +34,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class Saml2AuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5225098478444036532L;
|
||||
|
||||
private final RelyingPartyRegistration relyingPartyRegistration;
|
||||
|
||||
private final String saml2Response;
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
* </pre>
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration {
|
||||
|
||||
OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) {
|
||||
|
||||
@@ -254,6 +254,11 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||
|
||||
/**
|
||||
* Convenience method, mainly for testing.
|
||||
* <p>
|
||||
* Attempt to find the matching filter chain based on the given {@code url}. Note that
|
||||
* the URI is often not enough information and this method should be used with
|
||||
* caution. Instead, consider using Spring Security's testing support that mocks a
|
||||
* full HTTP request.
|
||||
* @param url the URL
|
||||
* @return matching filter list
|
||||
*/
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ import org.springframework.security.web.FilterInvocation;
|
||||
* {@link AuthorizationManager}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
class WebExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<FilterInvocation> {
|
||||
|
||||
private final Expression authorizeExpression;
|
||||
|
||||
+7
-2
@@ -112,9 +112,14 @@ public abstract class AbstractAuthenticationTargetUrlRequestHandler {
|
||||
trace("Using url %s from request parameter %s", targetUrlParameterValue, this.targetUrlParameter);
|
||||
return targetUrlParameterValue;
|
||||
}
|
||||
|
||||
String refererHeader = request.getHeader("Referer");
|
||||
if (!StringUtils.hasText(refererHeader)) {
|
||||
return this.defaultTargetUrl;
|
||||
}
|
||||
if (this.useReferer) {
|
||||
trace("Using url %s from Referer header", request.getHeader("Referer"));
|
||||
return request.getHeader("Referer");
|
||||
trace("Using url %s from Referer header", refererHeader);
|
||||
return refererHeader;
|
||||
}
|
||||
return this.defaultTargetUrl;
|
||||
}
|
||||
|
||||
+5
-5
@@ -103,11 +103,11 @@ public class HttpSessionRequestCache implements RequestCache {
|
||||
@Override
|
||||
public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (this.matchingRequestParameterName != null) {
|
||||
if (!StringUtils.hasText(request.getQueryString())
|
||||
|| !UriComponentsBuilder.fromUriString(UrlUtils.buildRequestUrl(request))
|
||||
.build()
|
||||
.getQueryParams()
|
||||
.containsKey(this.matchingRequestParameterName)) {
|
||||
if (!StringUtils.hasText(request.getQueryString()) || !UriComponentsBuilder.newInstance()
|
||||
.query(request.getQueryString())
|
||||
.build()
|
||||
.getQueryParams()
|
||||
.containsKey(this.matchingRequestParameterName)) {
|
||||
this.logger.trace(
|
||||
"matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided");
|
||||
return null;
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@ public final class ServerOneTimeTokenAuthenticationConverter implements ServerAu
|
||||
Assert.notNull(exchange, "exchange cannot be null");
|
||||
if (isFormEncodedRequest(exchange.getRequest())) {
|
||||
return exchange.getFormData()
|
||||
.map((data) -> OneTimeTokenAuthenticationToken.unauthenticated(data.getFirst(TOKEN)));
|
||||
.mapNotNull((data) -> data.getFirst(TOKEN))
|
||||
.map((data) -> OneTimeTokenAuthenticationToken.unauthenticated(data));
|
||||
}
|
||||
String token = resolveTokenFromRequest(exchange.getRequest());
|
||||
if (!StringUtils.hasText(token)) {
|
||||
|
||||
+13
@@ -305,6 +305,19 @@ public final class PathPatternRequestMatcher implements RequestMatcher {
|
||||
return this.method.name().equals(request.getMethod());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof HttpMethodRequestMatcher that)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(this.method, that.method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpMethod [" + this.method + "]";
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
|
||||
}
|
||||
|
||||
private void checkContentLengthHeader(String name, String value) {
|
||||
if ("Content-Length".equalsIgnoreCase(name)) {
|
||||
if (value != null && "Content-Length".equalsIgnoreCase(name)) {
|
||||
setContentLength(Long.parseLong(value));
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
|
||||
|
||||
@Override
|
||||
public PrintWriter append(CharSequence csq) {
|
||||
checkContentLength(csq.length());
|
||||
checkContentLength((csq != null) ? csq.length() : 4);
|
||||
return this.delegate.append(csq);
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.web.webauthn.api;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -34,6 +35,9 @@ import java.util.List;
|
||||
*/
|
||||
public final class AuthenticatorAttestationResponse extends AuthenticatorResponse {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -1628559840895428945L;
|
||||
|
||||
private final Bytes attestationObject;
|
||||
|
||||
private final List<AuthenticatorTransport> transports;
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.web.webauthn.management;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthorizationManager} that grants access when the {@link CredentialRecord}
|
||||
* identified by the provided credential id is owned by the currently authenticated user.
|
||||
*
|
||||
* <p>
|
||||
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
|
||||
* specification</a>, a credential id must contain at least 16 bytes with at least 100
|
||||
* bits of entropy, making it practically unguessable. The specification also advises that
|
||||
* credential ids should be kept private, as exposing them can leak personally identifying
|
||||
* information (see
|
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§ 14.6.3
|
||||
* Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is therefore
|
||||
* intended as defense in depth: even if a credential id were somehow exposed, an
|
||||
* unauthorized user could not delete another user's credential.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.5.10
|
||||
*/
|
||||
public final class CredentialRecordOwnerAuthorizationManager implements AuthorizationManager<Bytes> {
|
||||
|
||||
private final AuthenticatedAuthorizationManager<Bytes> authenticatedAuthorizationManager = AuthenticatedAuthorizationManager
|
||||
.authenticated();
|
||||
|
||||
private final UserCredentialRepository userCredentials;
|
||||
|
||||
private final PublicKeyCredentialUserEntityRepository userEntities;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param userCredentials the {@link UserCredentialRepository} to use
|
||||
* @param userEntities the {@link PublicKeyCredentialUserEntityRepository} to use
|
||||
*/
|
||||
public CredentialRecordOwnerAuthorizationManager(UserCredentialRepository userCredentials,
|
||||
PublicKeyCredentialUserEntityRepository userEntities) {
|
||||
Assert.notNull(userCredentials, "userCredentials cannot be null");
|
||||
Assert.notNull(userEntities, "userEntities cannot be null");
|
||||
this.userCredentials = userCredentials;
|
||||
this.userEntities = userEntities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, Bytes credentialId) {
|
||||
AuthorizationDecision decision = this.authenticatedAuthorizationManager.check(authentication, credentialId);
|
||||
if (!decision.isGranted()) {
|
||||
return decision;
|
||||
}
|
||||
Authentication auth = authentication.get();
|
||||
CredentialRecord credential = this.userCredentials.findByCredentialId(credentialId);
|
||||
if (credential == null) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
if (credential.getUserEntityUserId() == null) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
PublicKeyCredentialUserEntity userEntity = this.userEntities.findByUsername(auth.getName());
|
||||
if (userEntity == null) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
return new AuthorizationDecision(credential.getUserEntityUserId().equals(userEntity.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
+58
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.web.webauthn.registration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
@@ -34,6 +35,12 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
@@ -87,6 +94,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final UserCredentialRepository userCredentials;
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
||||
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
|
||||
|
||||
@@ -98,6 +108,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher.withDefaults()
|
||||
.matcher(HttpMethod.DELETE, "/webauthn/register/{id}");
|
||||
|
||||
private AuthorizationManager<Bytes> deleteCredentialAuthorizationManager = SingleResultAuthorizationManager
|
||||
.denyAll();
|
||||
|
||||
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
|
||||
WebAuthnRelyingPartyOperations rpOptions) {
|
||||
Assert.notNull(userCredentials, "userCredentials must not be null");
|
||||
@@ -132,6 +145,42 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
this.removeCredentialMatcher = removeCredentialMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthorizationManager} used to authorize the delete credential
|
||||
* operation. The object being authorized is the credential id as {@link Bytes}. By
|
||||
* default, all delete requests are denied.
|
||||
*
|
||||
* <p>
|
||||
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
|
||||
* specification</a>, a credential id must contain at least 16 bytes with at least 100
|
||||
* bits of entropy, making it practically unguessable. The specification also advises
|
||||
* that credential ids should be kept private, as exposing them can leak personally
|
||||
* identifying information (see
|
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§
|
||||
* 14.6.3 Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is
|
||||
* therefore intended as defense in depth: even if a credential id were somehow
|
||||
* exposed, an unauthorized user could not delete another user's credential.
|
||||
* @param deleteCredentialAuthorizationManager the {@link AuthorizationManager} to use
|
||||
* @since 6.5.10
|
||||
*/
|
||||
public void setDeleteCredentialAuthorizationManager(
|
||||
AuthorizationManager<Bytes> deleteCredentialAuthorizationManager) {
|
||||
Assert.notNull(deleteCredentialAuthorizationManager, "deleteCredentialAuthorizationManager cannot be null");
|
||||
this.deleteCredentialAuthorizationManager = deleteCredentialAuthorizationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextHolderStrategy} to use. The default is
|
||||
* {@link SecurityContextHolder#getContextHolderStrategy()}.
|
||||
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
|
||||
* use
|
||||
* @since 6.5.10
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
||||
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
@@ -203,7 +252,15 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
|
||||
private void removeCredential(HttpServletRequest request, HttpServletResponse response, String id)
|
||||
throws IOException {
|
||||
this.userCredentials.delete(Bytes.fromBase64(id));
|
||||
Bytes credentialId = Bytes.fromBase64(id);
|
||||
Supplier<Authentication> authentication = () -> this.securityContextHolderStrategy.getContext()
|
||||
.getAuthentication();
|
||||
AuthorizationResult result = this.deleteCredentialAuthorizationManager.authorize(authentication, credentialId);
|
||||
if (result != null && !result.isGranted()) {
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
return;
|
||||
}
|
||||
this.userCredentials.delete(credentialId);
|
||||
response.setStatus(HttpStatus.NO_CONTENT.value());
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -114,4 +114,12 @@ public class AbstractAuthenticationTargetUrlRequestHandlerTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setRedirectStrategy(null));
|
||||
}
|
||||
|
||||
// gh-18805
|
||||
@Test
|
||||
void returnDefaultUrlIfUseRefererIsTrueAndRefererHeaderIsEmpty() {
|
||||
this.handler.setUseReferer(true);
|
||||
this.request.addHeader("Referer", "");
|
||||
assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
@@ -168,6 +168,21 @@ public class HttpSessionRequestCacheTests {
|
||||
verify(request, never()).getParameterMap();
|
||||
}
|
||||
|
||||
// gh-16656
|
||||
@Test
|
||||
public void getMatchingRequestWhenMatchingRequestPathContainsPercentSignThenLookedUp() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setServletPath("/30 % off");
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setServletPath("/30 % off");
|
||||
requestToMatch.setQueryString("continue");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
}
|
||||
|
||||
private static final class CustomSavedRequest implements SavedRequest {
|
||||
|
||||
private final SavedRequest delegate;
|
||||
|
||||
+12
@@ -72,6 +72,18 @@ public class ServerOneTimeTokenAuthenticationConverterTests {
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
// gh-18973
|
||||
@Test
|
||||
void convertWhenNoTokenFormParameterThenNull() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body("username=Max");
|
||||
|
||||
Authentication authentication = this.converter.convert(MockServerWebExchange.from(request)).block();
|
||||
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertWhenTokenEncodedFormParameterThenReturnOneTimeTokenAuthenticationToken() {
|
||||
// @formatter:off
|
||||
|
||||
+11
@@ -145,6 +145,17 @@ public class PathPatternRequestMatcherTests {
|
||||
assertThat(matcher.matches(mock)).isTrue();
|
||||
}
|
||||
|
||||
// gh-18911
|
||||
@Test
|
||||
void testEqualsWithSameAndDifferentHttpMethod() {
|
||||
PathPatternRequestMatcher.Builder builder = PathPatternRequestMatcher.withDefaults();
|
||||
PathPatternRequestMatcher matcher1 = builder.matcher(HttpMethod.GET, "/foo");
|
||||
PathPatternRequestMatcher matcher2 = builder.matcher(HttpMethod.GET, "/foo");
|
||||
PathPatternRequestMatcher matcher3 = builder.matcher(HttpMethod.POST, "/foo");
|
||||
assertThat(matcher1).isEqualTo(matcher2);
|
||||
assertThat(matcher1).isNotEqualTo(matcher3);
|
||||
}
|
||||
|
||||
MockHttpServletRequest request(String uri) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
|
||||
+115
@@ -28,7 +28,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -1006,6 +1009,16 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.addHeader(null, "value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addHeaderNullValueDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.addHeader(HttpHeaders.CONTENT_LENGTH, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
@@ -1015,6 +1028,11 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIntHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.addIntHeader(null, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
@@ -1024,6 +1042,16 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.setHeader(null, "value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderNullValueDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.setHeader(HttpHeaders.CONTENT_LENGTH, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
@@ -1033,6 +1061,11 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIntHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.setIntHeader(null, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bufferSizePrintWriterWriteCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
@@ -1054,4 +1087,86 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintNullStringDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().print(s));
|
||||
verify(this.writer).print(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintlnNullStringDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().println(s));
|
||||
verify(this.writer).println(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintNullObjectDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
Object obj = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().print(obj));
|
||||
verify(this.writer).print(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintlnNullObjectDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
Object obj = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().println(obj));
|
||||
verify(this.writer).println(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterWriteNullStringDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().write(s));
|
||||
verify(this.writer).write(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterAppendNullCharSequenceDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
CharSequence csq = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().append(csq));
|
||||
verify(this.writer).append(csq);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterAppendNullCharSequenceIntIntDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
CharSequence csq = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().append(csq, 0, 3));
|
||||
verify(this.writer).append(csq, 0, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outputStreamPrintNullStringDoesNotThrow() throws Exception {
|
||||
givenGetOutputStreamThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getOutputStream().print(s));
|
||||
verify(this.out).print(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outputStreamPrintlnNullStringDoesNotThrow() throws Exception {
|
||||
givenGetOutputStreamThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getOutputStream().println(s));
|
||||
verify(this.out).println(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendErrorWithNullMsgDoesNotThrow() throws Exception {
|
||||
assertThatNoException().isThrownBy(() -> this.response.sendError(400, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendRedirectWithNullLocationDoesNotThrow() throws Exception {
|
||||
assertThatNoException().isThrownBy(() -> this.response.sendRedirect(null));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.web.webauthn.management;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestBytes;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link CredentialRecordOwnerAuthorizationManager}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.5.10
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CredentialRecordOwnerAuthorizationManagerTests {
|
||||
|
||||
@Mock
|
||||
private UserCredentialRepository userCredentials;
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialUserEntityRepository userEntities;
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserCredentialsThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(null, this.userEntities));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserEntitiesTonIllegalArgument() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(this.userCredentials, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenAuthenticationNullThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
AuthorizationDecision decision = manager.check(() -> null, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenNotAuthenticatedThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
|
||||
authentication.setAuthenticated(false);
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialNotFoundThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialUserEntityUserIdNullThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().userEntityUserId(null).build());
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenUserEntityNotFoundThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().build());
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialBelongsToUserThenGranted() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
Bytes userId = TestCredentialRecords.userCredential().build().getUserEntityUserId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().build());
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(userId)
|
||||
.displayName("User")
|
||||
.build();
|
||||
given(this.userEntities.findByUsername("user")).willReturn(userEntity);
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialBelongsToDifferentUserThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().build());
|
||||
PublicKeyCredentialUserEntity otherUserEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(TestBytes.get())
|
||||
.displayName("User")
|
||||
.build();
|
||||
given(this.userEntities.findByUsername("user")).willReturn(otherUserEntity);
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
+33
@@ -31,7 +31,11 @@ import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
@@ -213,12 +217,41 @@ class WebAuthnRegistrationFilterTests {
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
|
||||
this.filter.setDeleteCredentialAuthorizationManager(SingleResultAuthorizationManager.permitAll());
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||
.buildRequest(new MockServletContext());
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setDeleteCredentialAuthorizationManagerWhenNullThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setDeleteCredentialAuthorizationManager(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteAndCustomAuthorizationManagerThenUses() throws Exception {
|
||||
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
|
||||
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(true));
|
||||
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||
.buildRequest(new MockServletContext());
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
verify(authorizationManager).authorize(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteAndAuthorizationDeniedThenForbidden() throws Exception {
|
||||
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
|
||||
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(false));
|
||||
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||
.buildRequest(new MockServletContext());
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest registerCredentialRequest(String body) {
|
||||
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
|
||||
.content(body)
|
||||
|
||||
Reference in New Issue
Block a user