Add Passkeys Support
Closes gh-13305
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.authentication;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.BDDMockito;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.SimpleSavedRequest;
|
||||
|
||||
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.BDDMockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HttpMessageConverterAuthenticationSuccessHandlerTests {
|
||||
|
||||
@Mock
|
||||
private HttpMessageConverter converter;
|
||||
|
||||
@Mock
|
||||
private RequestCache requestCache;
|
||||
|
||||
private HttpMessageConverterAuthenticationSuccessHandler handler = new HttpMessageConverterAuthenticationSuccessHandler();
|
||||
|
||||
private MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
private Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
|
||||
|
||||
@Test
|
||||
void setConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setRequestCacheWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setRequestCache(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationSuccessWhenDefaultsThenContextRoot() throws Exception {
|
||||
this.handler.onAuthenticationSuccess(this.request, this.response, this.authentication);
|
||||
String body = this.response.getContentAsString();
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"redirectUrl" : "/",
|
||||
"authenticated": true
|
||||
}""", body, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationSuccessWhenSavedRequestThenInResponse() throws Exception {
|
||||
SimpleSavedRequest savedRequest = new SimpleSavedRequest("/redirect");
|
||||
given(this.requestCache.getRequest(this.request, this.response)).willReturn(savedRequest);
|
||||
this.handler.setRequestCache(this.requestCache);
|
||||
this.handler.onAuthenticationSuccess(this.request, this.response, this.authentication);
|
||||
verify(this.requestCache).removeRequest(this.request, this.response);
|
||||
String body = this.response.getContentAsString();
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"redirectUrl" : "/redirect",
|
||||
"authenticated": true
|
||||
}""", body, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthenticationSuccessWhenCustomConverterThenInResponse() throws Exception {
|
||||
SimpleSavedRequest savedRequest = new SimpleSavedRequest("/redirect");
|
||||
given(this.requestCache.getRequest(this.request, this.response)).willReturn(savedRequest);
|
||||
String expectedBody = "Custom!";
|
||||
BDDMockito.doAnswer((invocation) -> {
|
||||
this.response.getWriter().write(expectedBody);
|
||||
return null;
|
||||
}).when(this.converter).write(any(), any(), any());
|
||||
this.handler.setRequestCache(this.requestCache);
|
||||
this.handler.setConverter(this.converter);
|
||||
this.handler.onAuthenticationSuccess(this.request, this.response, this.authentication);
|
||||
String body = this.response.getContentAsString();
|
||||
assertThat(body).isEqualTo(expectedBody);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class COSEAlgorithmIdentifierTests {
|
||||
|
||||
@Test
|
||||
void valuesContainsAll() {
|
||||
List<COSEAlgorithmIdentifier> allMembers = Arrays.stream(COSEAlgorithmIdentifier.class.getFields())
|
||||
.filter((f) -> f.getType().isAssignableFrom(COSEAlgorithmIdentifier.class))
|
||||
.map((f) -> (COSEAlgorithmIdentifier) getValue(f))
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
assertThat(COSEAlgorithmIdentifier.values()).containsExactlyInAnyOrderElementsOf(allMembers);
|
||||
}
|
||||
|
||||
private <T> T getValue(Field f) {
|
||||
try {
|
||||
return (T) f.get(null);
|
||||
}
|
||||
catch (IllegalAccessException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
public final class TestAuthenticatorAttestationResponse {
|
||||
|
||||
public static AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder createAuthenticatorAttestationResponse() {
|
||||
return AuthenticatorAttestationResponse.builder()
|
||||
.attestationObject(Bytes.fromBase64(
|
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEDWRLOHq0Wxw4cOkCemynKqlAQIDJiABIVgg4Hkrn2kbGmpZTdoDZUNrppo93OqgQV7ONzVvo5GLCFciWCCrf6yIQggq2BfZntawxRsBBbWG_FWkYAoU8yPipS-5hg-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk"))
|
||||
.clientDataJSON(Bytes.fromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicTdsQ2RkM1NWUXhkQy12OHBuUkFHRW4xQjJNLXQ3WkVDV1B3Q0FtaFd2YyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0"))
|
||||
.transports(AuthenticatorTransport.HYBRID, AuthenticatorTransport.INTERNAL);
|
||||
}
|
||||
|
||||
private TestAuthenticatorAttestationResponse() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
public final class TestCredentialRecord {
|
||||
|
||||
public static ImmutableCredentialRecord.ImmutableCredentialRecordBuilder userCredential() {
|
||||
return ImmutableCredentialRecord.builder()
|
||||
.label("label")
|
||||
.credentialId(Bytes.fromBase64("NauGCN7bZ5jEBwThcde51g"))
|
||||
.userEntityUserId(Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM"))
|
||||
.publicKey(ImmutablePublicKeyCose.fromBase64(
|
||||
"pQECAyYgASFYIC7DAiV_trHFPjieOxXbec7q2taBcgLnIi19zrUwVhCdIlggvN6riHORK_velHcTLFK_uJhyKK0oBkJqzNqR2E-2xf8="))
|
||||
.backupEligible(true)
|
||||
.backupState(true);
|
||||
}
|
||||
|
||||
private TestCredentialRecord() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
public final class TestPublicKeyCredential {
|
||||
|
||||
public static PublicKeyCredential.PublicKeyCredentialBuilder<AuthenticatorAttestationResponse> createPublicKeyCredential() {
|
||||
AuthenticatorAttestationResponse response = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse()
|
||||
.build();
|
||||
return createPublicKeyCredential(response);
|
||||
}
|
||||
|
||||
public static <R extends AuthenticatorResponse> PublicKeyCredential.PublicKeyCredentialBuilder<R> createPublicKeyCredential(
|
||||
R response) {
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
return PublicKeyCredential.builder()
|
||||
.id("AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM")
|
||||
.rawId(Bytes
|
||||
.fromBase64("AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM"))
|
||||
.response(response)
|
||||
.type(PublicKeyCredentialType.PUBLIC_KEY)
|
||||
.clientExtensionResults(clientExtensionResults);
|
||||
}
|
||||
|
||||
private TestPublicKeyCredential() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public final class TestPublicKeyCredentialCreationOptions {
|
||||
|
||||
public static PublicKeyCredentialCreationOptions.PublicKeyCredentialCreationOptionsBuilder createPublicKeyCredentialCreationOptions() {
|
||||
|
||||
AuthenticatorSelectionCriteria authenticatorSelection = AuthenticatorSelectionCriteria.builder()
|
||||
.userVerification(UserVerificationRequirement.PREFERRED)
|
||||
.residentKey(ResidentKeyRequirement.REQUIRED)
|
||||
.build();
|
||||
Bytes challenge = Bytes.fromBase64("q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc");
|
||||
PublicKeyCredentialRpEntity rp = PublicKeyCredentialRpEntity.builder()
|
||||
.id("example.localhost")
|
||||
.name("SimpleWebAuthn Example")
|
||||
.build();
|
||||
Bytes userId = Bytes.fromBase64("oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w");
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.displayName("user@example.localhost")
|
||||
.id(userId)
|
||||
.name("user@example.localhost")
|
||||
.build();
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
ImmutableAuthenticationExtensionsClientInput.credProps);
|
||||
return PublicKeyCredentialCreationOptions.builder()
|
||||
.attestation(AttestationConveyancePreference.DIRECT)
|
||||
.user(userEntity)
|
||||
.pubKeyCredParams(PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.ES256,
|
||||
PublicKeyCredentialParameters.RS256)
|
||||
.authenticatorSelection(authenticatorSelection)
|
||||
.challenge(challenge)
|
||||
.rp(rp)
|
||||
.extensions(clientInputs)
|
||||
.timeout(Duration.ofMinutes(5));
|
||||
}
|
||||
|
||||
private TestPublicKeyCredentialCreationOptions() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public final class TestPublicKeyCredentialRequestOptions {
|
||||
|
||||
public static PublicKeyCredentialRequestOptions.PublicKeyCredentialRequestOptionsBuilder create() {
|
||||
return PublicKeyCredentialRequestOptions.builder()
|
||||
.timeout(Duration.ofMinutes(5))
|
||||
.rpId("example.localhost")
|
||||
.userVerification(UserVerificationRequirement.PREFERRED)
|
||||
.challenge(Bytes.fromBase64("cQfdGrj9zDg3zNBkOH3WPL954FTOShVy0-CoNgSewNM"));
|
||||
}
|
||||
|
||||
private TestPublicKeyCredentialRequestOptions() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.api;
|
||||
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity.PublicKeyCredentialUserEntityBuilder;
|
||||
|
||||
public final class TestPublicKeyCredentialUserEntity {
|
||||
|
||||
public static PublicKeyCredentialUserEntityBuilder userEntity() {
|
||||
return ImmutablePublicKeyCredentialUserEntity.builder().name("user").id(Bytes.random()).displayName("user");
|
||||
}
|
||||
|
||||
private TestPublicKeyCredentialUserEntity() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.authentication;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpSessionPublicKeyCredentialRequestOptionsRepository}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
class HttpSessionPublicKeyCredentialRequestOptionsRepositoryTests {
|
||||
|
||||
private HttpSessionPublicKeyCredentialRequestOptionsRepository repository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
|
||||
|
||||
private MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
@Test
|
||||
void integrationTests() {
|
||||
PublicKeyCredentialRequestOptions expected = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
this.repository.save(this.request, this.response, expected);
|
||||
Object attrValue = this.request.getSession()
|
||||
.getAttribute(HttpSessionPublicKeyCredentialRequestOptionsRepository.DEFAULT_ATTR_NAME);
|
||||
assertThat(attrValue).isEqualTo(expected);
|
||||
PublicKeyCredentialRequestOptions loadOptions = this.repository.load(this.request);
|
||||
assertThat(loadOptions).isEqualTo(expected);
|
||||
|
||||
this.repository.save(this.request, this.response, null);
|
||||
|
||||
Object attrValueAfterRemoval = this.request.getSession()
|
||||
.getAttribute(HttpSessionPublicKeyCredentialRequestOptionsRepository.DEFAULT_ATTR_NAME);
|
||||
assertThat(attrValueAfterRemoval).isNull();
|
||||
assertThat(this.request.getSession().getAttributeNames())
|
||||
.doesNotHaveToString(HttpSessionPublicKeyCredentialRequestOptionsRepository.DEFAULT_ATTR_NAME);
|
||||
PublicKeyCredentialRequestOptions loadOptionsAfterRemoval = this.repository.load(this.request);
|
||||
assertThat(loadOptionsAfterRemoval).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenNullSessionThenDoesNotCreate() {
|
||||
PublicKeyCredentialRequestOptions options = this.repository.load(this.request);
|
||||
assertThat(options).isNull();
|
||||
assertThat(this.request.getSession(false)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenSetAttrThenCustomAttr() {
|
||||
PublicKeyCredentialRequestOptions expected = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
String customAttr = "custom-attr";
|
||||
this.repository.setAttrName(customAttr);
|
||||
this.repository.save(this.request, this.response, expected);
|
||||
assertThat(this.request.getSession().getAttribute(customAttr)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenSetAttrThenCustomAttr() {
|
||||
PublicKeyCredentialRequestOptions expected = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
String customAttr = "custom-attr";
|
||||
this.repository.setAttrName(customAttr);
|
||||
this.request.getSession().setAttribute(customAttr, expected);
|
||||
PublicKeyCredentialRequestOptions actual = this.repository.load(this.request);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.authentication;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.verifyNoInteractions;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
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.status;
|
||||
|
||||
/**
|
||||
* Tests for {@link PublicKeyCredentialRequestOptionsFilter}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PublicKeyCredentialRequestOptionsFilterTests {
|
||||
|
||||
@Mock
|
||||
private WebAuthnRelyingPartyOperations relyingPartyOperations;
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository;
|
||||
|
||||
@Mock
|
||||
private HttpMessageConverter<Object> converter;
|
||||
|
||||
@Mock
|
||||
private SecurityContextHolderStrategy contextHolderStrategy;
|
||||
|
||||
private PublicKeyCredentialRequestOptionsFilter filter;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.filter = new PublicKeyCredentialRequestOptionsFilter(this.relyingPartyOperations);
|
||||
this.filter.setRequestOptionsRepository(this.requestOptionsRepository);
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup().addFilter(this.filter).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNull() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new PublicKeyCredentialRequestOptionsFilter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNoMatch() throws Exception {
|
||||
this.mockMvc.perform(post("/nomatch"))
|
||||
.andExpect(status().isNotFound())
|
||||
.andDo((result) -> assertThat(result.getResponse().getContentAsString()).isEmpty());
|
||||
verifyNoInteractions(this.relyingPartyOperations, this.requestOptionsRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNotPost() throws Exception {
|
||||
this.mockMvc.perform(get("/webauthn/authenticate/options"))
|
||||
.andExpect(status().isNotFound())
|
||||
.andDo((result) -> assertThat(result.getResponse().getContentAsString()).isEmpty());
|
||||
verifyNoInteractions(this.relyingPartyOperations, this.requestOptionsRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenMatches() throws Exception {
|
||||
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
given(this.relyingPartyOperations.createCredentialRequestOptions(any())).willReturn(options);
|
||||
|
||||
PublicKeyCredentialCreationOptions mockResult = this.relyingPartyOperations
|
||||
.createPublicKeyCredentialCreationOptions(null);
|
||||
this.mockMvc.perform(post("/webauthn/authenticate/options"))
|
||||
.andExpect(status().isOk())
|
||||
.andDo((result) -> JSONAssert.assertEquals(result.getResponse().getContentAsString(), """
|
||||
{
|
||||
"challenge": "cQfdGrj9zDg3zNBkOH3WPL954FTOShVy0-CoNgSewNM",
|
||||
"timeout": 300000,
|
||||
"rpId": "example.localhost",
|
||||
"allowCredentials": [],
|
||||
"userVerification": "preferred",
|
||||
"extensions": {}
|
||||
}
|
||||
""", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenCustom() throws Exception {
|
||||
String body = "custom body";
|
||||
willAnswer(new Answer<Void>() {
|
||||
@Override
|
||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
ServletServerHttpResponse response = invocation.getArgument(2);
|
||||
response.getBody().write(body.getBytes(StandardCharsets.UTF_8));
|
||||
return null;
|
||||
}
|
||||
}).given(this.converter).write(any(), any(), any());
|
||||
given(this.contextHolderStrategy.getContext())
|
||||
.willReturn(new SecurityContextImpl(new TestingAuthenticationToken("user", "password", "ROLE_USER")));
|
||||
this.filter.setConverter(this.converter);
|
||||
this.filter.setSecurityContextHolderStrategy(this.contextHolderStrategy);
|
||||
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
given(this.relyingPartyOperations.createCredentialRequestOptions(any())).willReturn(options);
|
||||
|
||||
PublicKeyCredentialCreationOptions mockResult = this.relyingPartyOperations
|
||||
.createPublicKeyCredentialCreationOptions(null);
|
||||
this.mockMvc.perform(post("/webauthn/authenticate/options"))
|
||||
.andExpect(status().isOk())
|
||||
.andDo((result) -> assertThat(result.getResponse().getContentAsString()).isEqualTo(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setConverterWhenNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setSecurityContextHolderStrategyWhenNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setSecurityContextHolderStrategy(null));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.authentication;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
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.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebAuthnAuthenticationFilter}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WebAuthnAuthenticationFilterTests {
|
||||
|
||||
private static final String VALID_BODY = """
|
||||
{
|
||||
"id": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||
"response": {
|
||||
"authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||
"signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
|
||||
"userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
|
||||
},
|
||||
"clientExtensionResults": {},
|
||||
"authenticatorAttachment": "platform"
|
||||
}
|
||||
""";
|
||||
|
||||
@Mock
|
||||
private GenericHttpMessageConverter<Object> converter;
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository;
|
||||
|
||||
@Mock
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
private MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
@Mock
|
||||
private FilterChain chain;
|
||||
|
||||
private WebAuthnAuthenticationFilter filter = new WebAuthnAuthenticationFilter();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.filter.setAuthenticationManager(this.authenticationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setConverterWhenNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setRequestOptionsRepositoryWhenNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRequestOptionsRepository(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenUrlDoesNotMatchThenChainContinues() throws Exception {
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
this.filter.setConverter(this.converter);
|
||||
this.filter.setRequestOptionsRepository(this.requestOptionsRepository);
|
||||
this.filter.doFilter(post("/nomatch").buildRequest(new MockServletContext()), response, this.chain);
|
||||
verifyNoInteractions(this.converter, this.requestOptionsRepository, response);
|
||||
verify(this.chain).doFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenMethodDoesNotMatchThenChainContinues() throws Exception {
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
this.filter.setConverter(this.converter);
|
||||
this.filter.setRequestOptionsRepository(this.requestOptionsRepository);
|
||||
this.filter.doFilter(get("/login/webauthn").buildRequest(new MockServletContext()), response, this.chain);
|
||||
verifyNoInteractions(this.converter, this.requestOptionsRepository, response);
|
||||
verify(this.chain).doFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNoBodyThenUnauthorized() throws Exception {
|
||||
this.filter.doFilter(matchingRequest(""), this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenInvalidJsonThenUnauthorized() throws Exception {
|
||||
MockHttpServletRequest request = matchingRequest("<>");
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenOptionsNullThenUnAuthorized() throws Exception {
|
||||
MockHttpServletRequest request = matchingRequest(VALID_BODY);
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenValidThenOk() throws Exception {
|
||||
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
given(this.requestOptionsRepository.load(any())).willReturn(options);
|
||||
PublicKeyCredentialUserEntity principal = TestPublicKeyCredentialUserEntity.userEntity().build();
|
||||
WebAuthnAuthentication authentication = new WebAuthnAuthentication(principal,
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
given(this.authenticationManager.authenticate(any())).willReturn(authentication);
|
||||
this.filter.setRequestOptionsRepository(this.requestOptionsRepository);
|
||||
MockHttpServletRequest request = matchingRequest(VALID_BODY);
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
verify(this.requestOptionsRepository).save(any(), any(), isNull());
|
||||
ArgumentCaptor<WebAuthnAuthenticationRequestToken> authenticationCaptor = ArgumentCaptor
|
||||
.forClass(WebAuthnAuthenticationRequestToken.class);
|
||||
verify(this.authenticationManager).authenticate(authenticationCaptor.capture());
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
WebAuthnAuthenticationRequestToken token = authenticationCaptor.getValue();
|
||||
assertThat(token).isNotNull();
|
||||
RelyingPartyAuthenticationRequest authnRequest = token.getWebAuthnRequest();
|
||||
PublicKeyCredential<AuthenticatorAssertionResponse> publicKey = authnRequest.getPublicKey();
|
||||
AuthenticatorAssertionResponse assertionResponse = publicKey.getResponse();
|
||||
assertThat(publicKey.getId()).isEqualTo("dYF7EGnRFFIXkpXi9XU2wg");
|
||||
assertThat(publicKey.getRawId().toBase64UrlString()).isEqualTo("dYF7EGnRFFIXkpXi9XU2wg");
|
||||
assertThat(assertionResponse.getAuthenticatorData().toBase64UrlString())
|
||||
.isEqualTo("y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA");
|
||||
assertThat(assertionResponse.getClientDataJSON().toBase64UrlString()).isEqualTo(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0");
|
||||
assertThat(assertionResponse.getSignature().toBase64UrlString()).isEqualTo(
|
||||
"MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z");
|
||||
assertThat(assertionResponse.getUserHandle().toBase64UrlString())
|
||||
.isEqualTo("Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo");
|
||||
assertThat(publicKey.getClientExtensionResults().getOutputs()).isEmpty();
|
||||
assertThat(authnRequest.getRequestOptions()).isEqualTo(options);
|
||||
assertThat(authnRequest.getPublicKey().getAuthenticatorAttachment())
|
||||
.isEqualTo(AuthenticatorAttachment.PLATFORM);
|
||||
String expectedBody = """
|
||||
{"redirectUrl":"/","authenticated":true}
|
||||
""";
|
||||
JSONAssert.assertEquals(expectedBody, this.response.getContentAsString(), false);
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest matchingRequest(String body) {
|
||||
return MockMvcRequestBuilders.post("/login/webauthn").content(body).buildRequest(new MockServletContext());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.authentication;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
class WebAuthnAuthenticationTests {
|
||||
|
||||
@Test
|
||||
void isAuthenticatedThenTrue() {
|
||||
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
WebAuthnAuthentication authentication = new WebAuthnAuthentication(userEntity, authorities);
|
||||
assertThat(authentication.isAuthenticated()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setAuthenticationWhenTrueThenException() {
|
||||
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
WebAuthnAuthentication authentication = new WebAuthnAuthentication(userEntity, authorities);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> authentication.setAuthenticated(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setAuthenticationWhenFalseThenNotAuthenticated() {
|
||||
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
WebAuthnAuthentication authentication = new WebAuthnAuthentication(userEntity, authorities);
|
||||
authentication.setAuthenticated(false);
|
||||
assertThat(authentication.isAuthenticated()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInputs;
|
||||
|
||||
/**
|
||||
* Test Jackson serialization of CredProtectAuthenticationExtensionsClientInput
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class CredProtectAuthenticationExtensionsClientInputJacksonTests {
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModule(new WebauthnJackson2Module());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationExtensionsClientInputsWhenCredProtectUserVerificationOptional() throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"credentialProtectionPolicy": "userVerificationOptional",
|
||||
"enforceCredentialProtectionPolicy": true
|
||||
}
|
||||
""";
|
||||
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = new CredProtectAuthenticationExtensionsClientInput.CredProtect(
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy.USER_VERIFICATION_OPTIONAL,
|
||||
true);
|
||||
CredProtectAuthenticationExtensionsClientInput credProtectInput = new CredProtectAuthenticationExtensionsClientInput(
|
||||
credProtect);
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
credProtectInput);
|
||||
|
||||
String actual = this.mapper.writeValueAsString(clientInputs);
|
||||
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationExtensionsClientInputsWhenCredProtectUserVerificationOptionalWithCredentialIdList()
|
||||
throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"credentialProtectionPolicy": "userVerificationOptionalWithCredentialIdList",
|
||||
"enforceCredentialProtectionPolicy": true
|
||||
}
|
||||
""";
|
||||
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = new CredProtectAuthenticationExtensionsClientInput.CredProtect(
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy.USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST,
|
||||
true);
|
||||
CredProtectAuthenticationExtensionsClientInput credProtectInput = new CredProtectAuthenticationExtensionsClientInput(
|
||||
credProtect);
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
credProtectInput);
|
||||
|
||||
String actual = this.mapper.writeValueAsString(clientInputs);
|
||||
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationExtensionsClientInputsWhenCredProtectUserVerificationRequired() throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"credentialProtectionPolicy": "userVerificationRequired",
|
||||
"enforceCredentialProtectionPolicy": true
|
||||
}
|
||||
""";
|
||||
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = new CredProtectAuthenticationExtensionsClientInput.CredProtect(
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy.USER_VERIFICATION_REQUIRED,
|
||||
true);
|
||||
CredProtectAuthenticationExtensionsClientInput credProtectInput = new CredProtectAuthenticationExtensionsClientInput(
|
||||
credProtect);
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
credProtectInput);
|
||||
|
||||
String actual = this.mapper.writeValueAsString(clientInputs);
|
||||
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationExtensionsClientInputsWhenEnforceCredentialProtectionPolicyTrue() throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"credentialProtectionPolicy": "userVerificationOptional",
|
||||
"enforceCredentialProtectionPolicy": true
|
||||
}
|
||||
""";
|
||||
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = new CredProtectAuthenticationExtensionsClientInput.CredProtect(
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy.USER_VERIFICATION_OPTIONAL,
|
||||
true);
|
||||
CredProtectAuthenticationExtensionsClientInput credProtectInput = new CredProtectAuthenticationExtensionsClientInput(
|
||||
credProtect);
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
credProtectInput);
|
||||
|
||||
String actual = this.mapper.writeValueAsString(clientInputs);
|
||||
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationExtensionsClientInputsWhenEnforceCredentialProtectionPolicyFalse() throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"credentialProtectionPolicy": "userVerificationOptional",
|
||||
"enforceCredentialProtectionPolicy": false
|
||||
}
|
||||
""";
|
||||
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = new CredProtectAuthenticationExtensionsClientInput.CredProtect(
|
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy.USER_VERIFICATION_OPTIONAL,
|
||||
false);
|
||||
CredProtectAuthenticationExtensionsClientInput credProtectInput = new CredProtectAuthenticationExtensionsClientInput(
|
||||
credProtect);
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
credProtectInput);
|
||||
|
||||
String actual = this.mapper.writeValueAsString(clientInputs);
|
||||
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.jackson;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
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.CredentialPropertiesOutput;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInput;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInputs;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class JacksonTests {
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModule(new WebauthnJackson2Module());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticatorTransport() throws Exception {
|
||||
AuthenticatorTransport transport = this.mapper.readValue("\"hybrid\"", AuthenticatorTransport.class);
|
||||
|
||||
assertThat(transport).isEqualTo(AuthenticatorTransport.HYBRID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticatorAttachment() throws Exception {
|
||||
AuthenticatorAttachment value = this.mapper.readValue("\"cross-platform\"", AuthenticatorAttachment.class);
|
||||
assertThat(value).isEqualTo(AuthenticatorAttachment.CROSS_PLATFORM);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticatorAttachment() throws Exception {
|
||||
String value = this.mapper.writeValueAsString(AuthenticatorAttachment.CROSS_PLATFORM);
|
||||
assertThat(value).isEqualTo("\"cross-platform\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputs() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"credProps": {
|
||||
"rk": false
|
||||
}
|
||||
}
|
||||
""";
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
|
||||
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
|
||||
AuthenticationExtensionsClientOutputs.class);
|
||||
assertThat(outputs).usingRecursiveComparison().isEqualTo(clientExtensionResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenAuthenticatorDisplayName() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"credProps": {
|
||||
"rk": false,
|
||||
"authenticatorDisplayName": "1Password"
|
||||
}
|
||||
}
|
||||
""";
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
|
||||
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
|
||||
AuthenticationExtensionsClientOutputs.class);
|
||||
assertThat(outputs).usingRecursiveComparison().isEqualTo(clientExtensionResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCredPropsWhenAuthenticatorDisplayName() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"rk": false,
|
||||
"authenticatorDisplayName": "1Password"
|
||||
}
|
||||
""";
|
||||
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
|
||||
|
||||
CredentialPropertiesOutput outputs = this.mapper.readValue(json, CredentialPropertiesOutput.class);
|
||||
assertThat(outputs).usingRecursiveComparison().isEqualTo(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenFieldAfter() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"clientOutputs": {
|
||||
"credProps": {
|
||||
"rk": false
|
||||
}
|
||||
},
|
||||
"label": "Cell Phone"
|
||||
}
|
||||
""";
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
|
||||
ClassWithOutputsAndAnotherField expected = new ClassWithOutputsAndAnotherField();
|
||||
expected.setClientOutputs(clientExtensionResults);
|
||||
expected.setLabel("Cell Phone");
|
||||
|
||||
ClassWithOutputsAndAnotherField actual = this.mapper.readValue(json, ClassWithOutputsAndAnotherField.class);
|
||||
assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writePublicKeyCredentialCreationOptions() throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"attestation": "direct",
|
||||
"authenticatorSelection": {
|
||||
"residentKey": "required"
|
||||
},
|
||||
"challenge": "q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc",
|
||||
"excludeCredentials": [],
|
||||
"extensions": {
|
||||
"credProps": true
|
||||
},
|
||||
"pubKeyCredParams": [
|
||||
{
|
||||
"alg": -7,
|
||||
"type": "public-key"
|
||||
},{
|
||||
"alg": -8,
|
||||
"type": "public-key"
|
||||
},
|
||||
{
|
||||
"alg": -257,
|
||||
"type": "public-key"
|
||||
}
|
||||
],
|
||||
"rp": {
|
||||
"id": "example.localhost",
|
||||
"name": "SimpleWebAuthn Example"
|
||||
},
|
||||
"timeout": 300000,
|
||||
"user": {
|
||||
"displayName": "user@example.localhost",
|
||||
"id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
|
||||
"name": "user@example.localhost"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
|
||||
String string = this.mapper.writeValueAsString(options);
|
||||
|
||||
JSONAssert.assertEquals(expected, string, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readPublicKeyCredentialAuthenticatorAttestationResponse() throws Exception {
|
||||
|
||||
PublicKeyCredential<AuthenticatorAttestationResponse> publicKeyCredential = this.mapper.readValue(
|
||||
PublicKeyCredentialJson.PUBLIC_KEY_JSON,
|
||||
new TypeReference<PublicKeyCredential<AuthenticatorAttestationResponse>>() {
|
||||
});
|
||||
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
|
||||
PublicKeyCredential<AuthenticatorAttestationResponse> expected = PublicKeyCredential.builder()
|
||||
.id("AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM")
|
||||
.rawId(Bytes
|
||||
.fromBase64("AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM"))
|
||||
.response(AuthenticatorAttestationResponse.builder()
|
||||
.attestationObject(Bytes.fromBase64(
|
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQF-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk"))
|
||||
.clientDataJSON(Bytes.fromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSUJRbnVZMVowSzFIcUJvRldDcDJ4bEpsOC1vcV9hRklYenlUX0YwLTBHVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"))
|
||||
.transports(AuthenticatorTransport.HYBRID, AuthenticatorTransport.INTERNAL)
|
||||
.build())
|
||||
.type(PublicKeyCredentialType.PUBLIC_KEY)
|
||||
.clientExtensionResults(clientExtensionResults)
|
||||
.authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
|
||||
.build();
|
||||
|
||||
assertThat(publicKeyCredential).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readPublicKeyCredentialAuthenticatorAttestationResponseWhenExtraFields() throws Exception {
|
||||
final String json = """
|
||||
{
|
||||
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQF-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk",
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSUJRbnVZMVowSzFIcUJvRldDcDJ4bEpsOC1vcV9hRklYenlUX0YwLTBHVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||
"transports": [
|
||||
"hybrid",
|
||||
"internal"
|
||||
],
|
||||
"publicKeyAlgorithm": -7,
|
||||
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkr7Z6k8TDS6Mc36C9WnYend5_wLNTfOrA7nKXHwvY6wrnHk6VMYQ_EtL7zlMAAG6bhqpUrgJJYnstgN2SO4EuQ",
|
||||
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQF-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk"
|
||||
}
|
||||
""";
|
||||
AuthenticatorAttestationResponse response = this.mapper.readValue(json, AuthenticatorAttestationResponse.class);
|
||||
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
|
||||
AuthenticatorAttestationResponse expected = AuthenticatorAttestationResponse.builder()
|
||||
.attestationObject(Bytes.fromBase64(
|
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQF-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk"))
|
||||
.clientDataJSON(Bytes.fromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSUJRbnVZMVowSzFIcUJvRldDcDJ4bEpsOC1vcV9hRklYenlUX0YwLTBHVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"))
|
||||
.transports(AuthenticatorTransport.HYBRID, AuthenticatorTransport.INTERNAL)
|
||||
.build();
|
||||
|
||||
assertThat(response).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationOptions() throws Exception {
|
||||
PublicKeyCredentialRequestOptions credentialRequestOptions = PublicKeyCredentialRequestOptions.builder()
|
||||
.allowCredentials(Arrays.asList())
|
||||
.challenge(Bytes.fromBase64("I69THX904Q8ONhCgUgOu2PCQCcEjTDiNmokdbgsAsYU"))
|
||||
.rpId("example.localhost")
|
||||
.timeout(Duration.ofMinutes(5))
|
||||
.userVerification(UserVerificationRequirement.REQUIRED)
|
||||
.build();
|
||||
String actual = this.mapper.writeValueAsString(credentialRequestOptions);
|
||||
|
||||
String expected = """
|
||||
{
|
||||
"challenge": "I69THX904Q8ONhCgUgOu2PCQCcEjTDiNmokdbgsAsYU",
|
||||
"allowCredentials": [],
|
||||
"timeout": 300000,
|
||||
"userVerification": "required",
|
||||
"rpId": "example.localhost"
|
||||
}
|
||||
|
||||
""";
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readPublicKeyCredentialAuthenticatorAssertionResponse() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"id": "IquGb208Fffq2cROa1ZxMg",
|
||||
"rawId": "IquGb208Fffq2cROa1ZxMg",
|
||||
"response": {
|
||||
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA",
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaDB2Z3dHUWpvQ3pBekRVc216UHBrLUpWSUpSUmduMEw0S1ZTWU5SY0VaYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||
"signature": "MEUCIAdfzPAn3voyXynwa0IXk1S0envMY5KP3NEe9aj4B2BuAiEAm_KJhQoWXdvfhbzwACU3NM4ltQe7_Il46qFUwtpuTdg",
|
||||
"userHandle": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w"
|
||||
},
|
||||
"type": "public-key",
|
||||
"clientExtensionResults": {},
|
||||
"authenticatorAttachment": "cross-platform"
|
||||
}
|
||||
""";
|
||||
PublicKeyCredential<AuthenticatorAssertionResponse> publicKeyCredential = this.mapper.readValue(json,
|
||||
new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse>>() {
|
||||
});
|
||||
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs();
|
||||
|
||||
PublicKeyCredential<AuthenticatorAssertionResponse> expected = PublicKeyCredential.builder()
|
||||
.id("IquGb208Fffq2cROa1ZxMg")
|
||||
.rawId(Bytes.fromBase64("IquGb208Fffq2cROa1ZxMg"))
|
||||
.response(AuthenticatorAssertionResponse.builder()
|
||||
.authenticatorData(Bytes.fromBase64("SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA"))
|
||||
.clientDataJSON(Bytes.fromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaDB2Z3dHUWpvQ3pBekRVc216UHBrLUpWSUpSUmduMEw0S1ZTWU5SY0VaYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"))
|
||||
.signature(Bytes.fromBase64(
|
||||
"MEUCIAdfzPAn3voyXynwa0IXk1S0envMY5KP3NEe9aj4B2BuAiEAm_KJhQoWXdvfhbzwACU3NM4ltQe7_Il46qFUwtpuTdg"))
|
||||
.userHandle(Bytes.fromBase64("oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w"))
|
||||
.build())
|
||||
.type(PublicKeyCredentialType.PUBLIC_KEY)
|
||||
.clientExtensionResults(clientExtensionResults)
|
||||
.authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
|
||||
.build();
|
||||
|
||||
assertThat(publicKeyCredential).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAuthenticationExtensionsClientInputsWhenCredPropsTrue() throws Exception {
|
||||
String expected = """
|
||||
{
|
||||
"credProps": true
|
||||
}
|
||||
""";
|
||||
|
||||
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
|
||||
ImmutableAuthenticationExtensionsClientInput.credProps);
|
||||
|
||||
String actual = this.mapper.writeValueAsString(clientInputs);
|
||||
|
||||
JSONAssert.assertEquals(expected, actual, false);
|
||||
}
|
||||
|
||||
public static class ClassWithOutputsAndAnotherField {
|
||||
|
||||
private String label;
|
||||
|
||||
private AuthenticationExtensionsClientOutputs clientOutputs;
|
||||
|
||||
public String getLabel() {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public AuthenticationExtensionsClientOutputs getClientOutputs() {
|
||||
return this.clientOutputs;
|
||||
}
|
||||
|
||||
public void setClientOutputs(AuthenticationExtensionsClientOutputs clientOutputs) {
|
||||
this.clientOutputs = clientOutputs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.jackson;
|
||||
|
||||
/**
|
||||
* JSON for {@code PublicKeyCredential<AuthenticatorAttestationResponse>}
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
public final class PublicKeyCredentialJson {
|
||||
|
||||
public static final String PUBLIC_KEY_JSON = """
|
||||
{
|
||||
"id": "AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM",
|
||||
"rawId": "AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM",
|
||||
"response": {
|
||||
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQF-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk",
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSUJRbnVZMVowSzFIcUJvRldDcDJ4bEpsOC1vcV9hRklYenlUX0YwLTBHVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||
"transports": [
|
||||
"hybrid",
|
||||
"internal"
|
||||
]
|
||||
},
|
||||
"type": "public-key",
|
||||
"clientExtensionResults": {
|
||||
"credProps": {
|
||||
"rk": false
|
||||
}
|
||||
},
|
||||
"authenticatorAttachment": "cross-platform"
|
||||
}
|
||||
""";
|
||||
|
||||
private PublicKeyCredentialJson() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MapPublicKeyCredentialUserEntityRepository}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
class MapPublicKeyCredentialUserEntityRepositoryTests {
|
||||
|
||||
private MapPublicKeyCredentialUserEntityRepository userEntities = new MapPublicKeyCredentialUserEntityRepository();
|
||||
|
||||
private String username = "username";
|
||||
|
||||
private PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity()
|
||||
.name(this.username)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void findByIdWhenExistsThenFound() {
|
||||
this.userEntities.save(this.userEntity);
|
||||
PublicKeyCredentialUserEntity findById = this.userEntities.findById(this.userEntity.getId());
|
||||
assertThat(findById).isEqualTo(this.userEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByIdWhenDoesNotExistThenNull() {
|
||||
PublicKeyCredentialUserEntity findById = this.userEntities.findById(this.userEntity.getId());
|
||||
assertThat(findById).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByUsernameWhenExistsThenFound() {
|
||||
this.userEntities.save(this.userEntity);
|
||||
PublicKeyCredentialUserEntity foundUserEntity = this.userEntities.findByUsername(this.username);
|
||||
assertThat(foundUserEntity).isEqualTo(this.userEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByUsernameReturnsNullWhenUsernameDoesNotExist() {
|
||||
PublicKeyCredentialUserEntity foundUserEntity = this.userEntities.findByUsername(this.username);
|
||||
assertThat(foundUserEntity).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenNonNullThenSuccess() {
|
||||
this.userEntities.save(this.userEntity);
|
||||
PublicKeyCredentialUserEntity foundUserEntity = this.userEntities.findByUsername(this.username);
|
||||
assertThat(foundUserEntity).isEqualTo(this.userEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenUpdateThenUpdated() {
|
||||
PublicKeyCredentialUserEntity newUserEntity = TestPublicKeyCredentialUserEntity.userEntity()
|
||||
.name(this.userEntity.getName())
|
||||
.displayName("Updated")
|
||||
.build();
|
||||
this.userEntities.save(this.userEntity);
|
||||
this.userEntities.save(newUserEntity);
|
||||
PublicKeyCredentialUserEntity foundUserEntity = this.userEntities.findByUsername(this.username);
|
||||
assertThat(foundUserEntity).isEqualTo(newUserEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhenExistsThenRemovesExistingEntry() {
|
||||
this.userEntities.save(this.userEntity);
|
||||
this.userEntities.delete(this.userEntity.getId());
|
||||
assertThat(this.userEntities.findByUsername(this.username)).isNull();
|
||||
assertThat(this.userEntities.findById(this.userEntity.getId())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhenNullAndDoesNotExistThenNoException() {
|
||||
assertThatNoException().isThrownBy(() -> this.userEntities.delete(this.userEntity.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecord;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MapUserCredentialRepository}
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
class MapUserCredentialRepositoryTests {
|
||||
|
||||
private final MapUserCredentialRepository userCredentials = new MapUserCredentialRepository();
|
||||
|
||||
@Test
|
||||
void findByUserIdWhenNotFoundThenEmpty() {
|
||||
assertThat(this.userCredentials.findByUserId(Bytes.random())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByUserIdWhenNullIdThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.userCredentials.findByUserId(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByCredentialIdWhenIdNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.userCredentials.findByCredentialId(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByCredentialIdWhenNotFoundThenIllegalArgumentException() {
|
||||
assertThat(this.userCredentials.findByCredentialId(Bytes.random())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhenCredentialNotFoundThenNoException() {
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
assertThatNoException().isThrownBy(() -> this.userCredentials.delete(credentialRecord.getCredentialId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhenNullIdThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.userCredentials.delete(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveThenFound() {
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
this.userCredentials.save(credentialRecord);
|
||||
assertThat(this.userCredentials.findByCredentialId(credentialRecord.getCredentialId()))
|
||||
.isEqualTo(credentialRecord);
|
||||
assertThat(this.userCredentials.findByUserId(credentialRecord.getUserEntityUserId()))
|
||||
.containsOnly(credentialRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.userCredentials.save(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAndDeleteThenNotFound() {
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
this.userCredentials.save(credentialRecord);
|
||||
this.userCredentials.delete(credentialRecord.getCredentialId());
|
||||
assertThat(this.userCredentials.findByCredentialId(credentialRecord.getCredentialId())).isNull();
|
||||
assertThat(this.userCredentials.findByUserId(credentialRecord.getUserEntityUserId())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenUpdateThenUpdated() {
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
this.userCredentials.save(credentialRecord);
|
||||
Instant updatedLastUsed = credentialRecord.getLastUsed().plusSeconds(120);
|
||||
CredentialRecord updatedCredentialRecord = ImmutableCredentialRecord.fromCredentialRecord(credentialRecord)
|
||||
.lastUsed(updatedLastUsed)
|
||||
.build();
|
||||
this.userCredentials.save(updatedCredentialRecord);
|
||||
assertThat(this.userCredentials.findByCredentialId(credentialRecord.getCredentialId()))
|
||||
.isEqualTo(updatedCredentialRecord);
|
||||
assertThat(this.userCredentials.findByUserId(credentialRecord.getUserEntityUserId()))
|
||||
.containsOnly(updatedCredentialRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenSameUserThenUpdated() {
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
this.userCredentials.save(credentialRecord);
|
||||
CredentialRecord newCredentialRecord = ImmutableCredentialRecord.fromCredentialRecord(credentialRecord)
|
||||
.credentialId(Bytes.random())
|
||||
.build();
|
||||
this.userCredentials.save(newCredentialRecord);
|
||||
assertThat(this.userCredentials.findByCredentialId(credentialRecord.getCredentialId()))
|
||||
.isEqualTo(credentialRecord);
|
||||
assertThat(this.userCredentials.findByCredentialId(newCredentialRecord.getCredentialId()))
|
||||
.isEqualTo(newCredentialRecord);
|
||||
assertThat(this.userCredentials.findByUserId(credentialRecord.getUserEntityUserId()))
|
||||
.containsOnly(credentialRecord, newCredentialRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenDifferentUserThenNewEntryAdded() {
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
this.userCredentials.save(credentialRecord);
|
||||
CredentialRecord newCredentialRecord = ImmutableCredentialRecord.fromCredentialRecord(credentialRecord)
|
||||
.userEntityUserId(Bytes.random())
|
||||
.credentialId(Bytes.random())
|
||||
.build();
|
||||
this.userCredentials.save(newCredentialRecord);
|
||||
assertThat(this.userCredentials.findByCredentialId(credentialRecord.getCredentialId()))
|
||||
.isEqualTo(credentialRecord);
|
||||
assertThat(this.userCredentials.findByCredentialId(newCredentialRecord.getCredentialId()))
|
||||
.isEqualTo(newCredentialRecord);
|
||||
assertThat(this.userCredentials.findByUserId(credentialRecord.getUserEntityUserId()))
|
||||
.containsOnly(credentialRecord);
|
||||
assertThat(this.userCredentials.findByUserId(newCredentialRecord.getUserEntityUserId()))
|
||||
.containsOnly(newCredentialRecord);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
|
||||
|
||||
public final class TestPublicKeyCredentialRpEntity {
|
||||
|
||||
public static PublicKeyCredentialRpEntity.PublicKeyCredentialRpEntityBuilder createRpEntity() {
|
||||
return PublicKeyCredentialRpEntity.builder().id("example.localhost").name("Spring Security Relying Party");
|
||||
}
|
||||
|
||||
private TestPublicKeyCredentialRpEntity() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.Base64Variants;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
|
||||
import com.webauthn4j.converter.AttestationObjectConverter;
|
||||
import com.webauthn4j.converter.util.ObjectConverter;
|
||||
import com.webauthn4j.data.attestation.AttestationObject;
|
||||
import com.webauthn4j.data.attestation.authenticator.AuthenticatorData;
|
||||
import com.webauthn4j.data.extension.authenticator.RegistrationExtensionAuthenticatorOutput;
|
||||
import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialParameters;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponse;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class Webauthn4jRelyingPartyOperationsTests {
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialUserEntityRepository userEntities;
|
||||
|
||||
@Mock
|
||||
private UserCredentialRepository userCredentials;
|
||||
|
||||
// AuthenticatorDataFlags.Bitmasks
|
||||
private static byte UP = 0x01;
|
||||
|
||||
private static byte UV = 0x04;
|
||||
|
||||
private static byte BE = 0x08;
|
||||
|
||||
private static byte BS = 0x10;
|
||||
|
||||
private Set<String> origins = Set.of("https://example.localhost:8443");
|
||||
|
||||
private UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken("user", "password",
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
|
||||
private PublicKeyCredentialRpEntity rpEntity = TestPublicKeyCredentialRpEntity.createRpEntity().build();
|
||||
|
||||
private Webauthn4JRelyingPartyOperations rpOperations;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.rpOperations = new Webauthn4JRelyingPartyOperations(this.userEntities, this.userCredentials, this.rpEntity,
|
||||
this.origins);
|
||||
}
|
||||
|
||||
String label = "Phone";
|
||||
|
||||
@Test
|
||||
void constructorWhenUserEntitiesNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new Webauthn4JRelyingPartyOperations(null, this.userCredentials, this.rpEntity, this.origins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenUserCredentialsNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new Webauthn4JRelyingPartyOperations(this.userEntities, null, this.rpEntity, this.origins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenRpEntityNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Webauthn4JRelyingPartyOperations(this.userEntities,
|
||||
this.userCredentials, null, this.origins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenOriginsNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Webauthn4JRelyingPartyOperations(this.userEntities,
|
||||
this.userCredentials, this.rpEntity, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublicKeyCredentialCreationOptionsWhenRequestNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.rpOperations.createPublicKeyCredentialCreationOptions(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublicKeyCredentialCreationOptionsWhenAnonymousThenIllegalArgumentException() {
|
||||
AnonymousAuthenticationToken anonymous = new AnonymousAuthenticationToken("key", "notAuthenticated",
|
||||
Set.of(() -> "ROLE_ANOYMOUS"));
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.rpOperations.createPublicKeyCredentialCreationOptions(
|
||||
new ImmutablePublicKeyCredentialCreationOptionsRequest(anonymous)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublicKeyCredentialCreationOptionsWhenNotIsAuthenticatedThenIllegalArgumentException() {
|
||||
UsernamePasswordAuthenticationToken notAuthenticated = new UsernamePasswordAuthenticationToken("user",
|
||||
"password");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.rpOperations.createPublicKeyCredentialCreationOptions(
|
||||
new ImmutablePublicKeyCredentialCreationOptionsRequest(notAuthenticated)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublicKeyCredentialCreationOptionsWhenDefaultsThenSuccess() {
|
||||
PublicKeyCredentialCreationOptions expectedCreationOptions = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.rp(this.rpEntity)
|
||||
.user(TestPublicKeyCredentialUserEntity.userEntity().build())
|
||||
.build();
|
||||
PublicKeyCredentialCreationOptions creationOptions = this.rpOperations.createPublicKeyCredentialCreationOptions(
|
||||
new ImmutablePublicKeyCredentialCreationOptionsRequest(this.user));
|
||||
assertThat(creationOptions).usingRecursiveComparison()
|
||||
.ignoringFields("challenge", "user.id")
|
||||
.isEqualTo(expectedCreationOptions);
|
||||
// https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp
|
||||
assertThat(creationOptions.getRp()).isNotNull();
|
||||
assertThat(creationOptions.getRp().getName()).describedAs("Its value’s name member is REQUIRED").isNotNull();
|
||||
// https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user
|
||||
PublicKeyCredentialUserEntity userEntity = creationOptions.getUser();
|
||||
assertThat(userEntity).isNotNull();
|
||||
assertThat(userEntity.getName()).isNotNull();
|
||||
assertThat(userEntity.getDisplayName()).isNotNull();
|
||||
// https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id
|
||||
Bytes userId = userEntity.getId();
|
||||
assertThat(userId).isNotNull();
|
||||
assertThat(userId.getBytes()).describedAs("user id is a maximum size of 64 bytes").hasSizeLessThanOrEqualTo(64);
|
||||
assertThat(userId.getBytes())
|
||||
.describedAs("we want enough entropy in the user id, so it should be at least 16 bytes")
|
||||
.hasSizeGreaterThanOrEqualTo(16);
|
||||
|
||||
Bytes challenge = creationOptions.getChallenge();
|
||||
assertThat(challenge).isNotNull();
|
||||
byte[] challengeBytes = challenge.getBytes();
|
||||
// https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges
|
||||
assertThat(challengeBytes).describedAs("Challenges should be at least 16 bytes")
|
||||
.hasSizeGreaterThanOrEqualTo(16);
|
||||
// https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams
|
||||
assertThat(creationOptions.getPubKeyCredParams()).describedAs(
|
||||
"Relying Parties that wish to support a wide range of authenticators SHOULD include at least the following COSEAlgorithmIdentifier values")
|
||||
.containsExactly(PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.ES256,
|
||||
PublicKeyCredentialParameters.RS256);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublicKeyCredentialCreationOptionsWhenCustomizeThenCustomized() {
|
||||
Duration overriddenTimeout = Duration.ofMinutes(10);
|
||||
this.rpOperations.setCustomizeCreationOptions((options) -> options.timeout(overriddenTimeout));
|
||||
PublicKeyCredentialCreationOptions creationOptions = this.rpOperations.createPublicKeyCredentialCreationOptions(
|
||||
new ImmutablePublicKeyCredentialCreationOptionsRequest(this.user));
|
||||
assertThat(creationOptions.getTimeout()).isEqualTo(overriddenTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublicKeyCredentialCreationOptionsWhenExcludesThenSuccess() {
|
||||
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
|
||||
CredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
|
||||
PublicKeyCredentialDescriptor descriptor = PublicKeyCredentialDescriptor.builder()
|
||||
.id(credentialRecord.getCredentialId())
|
||||
.transports(credentialRecord.getTransports())
|
||||
.build();
|
||||
given(this.userEntities.findByUsername(this.user.getName())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Arrays.asList(credentialRecord));
|
||||
PublicKeyCredentialCreationOptions creationOptions = this.rpOperations.createPublicKeyCredentialCreationOptions(
|
||||
new ImmutablePublicKeyCredentialCreationOptionsRequest(this.user));
|
||||
|
||||
RecursiveComparisonConfiguration configuration = RecursiveComparisonConfiguration.builder().build();
|
||||
assertThat(creationOptions.getExcludeCredentials()).usingRecursiveFieldByFieldElementComparator(configuration)
|
||||
.containsOnly(descriptor);
|
||||
}
|
||||
|
||||
// registerCredential
|
||||
|
||||
@Test
|
||||
void registerCredentialWhenRpRegistrationRequestNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.rpOperations.registerCredential(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerCredentialWhenExistsThenException() {
|
||||
PublicKeyCredentialCreationOptions creationOptions = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
PublicKeyCredential<AuthenticatorAttestationResponse> publicKeyCredential = TestPublicKeyCredential
|
||||
.createPublicKeyCredential()
|
||||
.build();
|
||||
RelyingPartyPublicKey rpPublicKey = new RelyingPartyPublicKey(publicKeyCredential, this.label);
|
||||
|
||||
ImmutableRelyingPartyRegistrationRequest rpRegistrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
creationOptions, rpPublicKey);
|
||||
given(this.userCredentials.findByCredentialId(publicKeyCredential.getRawId()))
|
||||
.willReturn(TestCredentialRecord.userCredential().build());
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(rpRegistrationRequest));
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 7. Verify that the value of C.type is webauthn.create.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenCTypeIsNotWebAuthn() {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
String originalClientDataJSON = new String(
|
||||
TestAuthenticatorAttestationResponse.createAuthenticatorAttestationResponse()
|
||||
.build()
|
||||
.getClientDataJSON()
|
||||
.getBytes());
|
||||
String invalidTypeClientDataJSON = originalClientDataJSON.replace("webauthn.create", "webauthn.INVALID");
|
||||
AuthenticatorAttestationResponseBuilder responseBldr = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse()
|
||||
.clientDataJSON(new Bytes(invalidTypeClientDataJSON.getBytes(StandardCharsets.UTF_8)));
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(responseBldr.build()).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("ClientData.type");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 8. Verify that the value of C.challengeBytes equals the base64url encoding of
|
||||
* options.challengeBytes.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenCChallengeNotEqualBase64UrlEncodingOptionsChallenge() {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
// change the expected challenge so it does not match
|
||||
.challenge(Bytes.fromBase64("h0vgwGQjoCzAzDUsmzPpk-JVIJRRgn0L4KVSYNRcEZc"))
|
||||
.build();
|
||||
AuthenticatorAttestationResponseBuilder responseBldr = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse();
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(responseBldr.build()).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("challenge");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 9. Verify that the value of C.origin is an origin expected by the Relying Party.
|
||||
* See § 13.4.9 Validating the origin of a credential for guidance.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenCOriginNotExpected() {
|
||||
this.rpOperations = new Webauthn4JRelyingPartyOperations(this.userEntities, this.userCredentials, this.rpEntity,
|
||||
Set.of("https://doesnotmatch.localhost:8443"));
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
AuthenticatorAttestationResponseBuilder responseBldr = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse();
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(responseBldr.build()).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("origin");
|
||||
}
|
||||
|
||||
// FIXME: Need to add 10. If C.topOrigin is present
|
||||
// https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 13. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected
|
||||
* by the Relying Party.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenClientDataJSONDoesNotMatchHash() {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.rp(PublicKeyCredentialRpEntity.builder().id("invalid").name("Spring Security").build())
|
||||
.build();
|
||||
AuthenticatorAttestationResponseBuilder responseBldr = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse();
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(responseBldr.build()).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("hash");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 14. Verify that the UP bit of the flags in authData is set.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenUPFlagsNotSet() throws Exception {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(setFlag(UP)).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("UP flag");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 15. If the Relying Party requires user verification for this registration, verify
|
||||
* that the UV bit of the flags in authData is set.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenUVBitNotSet() throws Exception {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
|
||||
.userVerification(UserVerificationRequirement.REQUIRED)
|
||||
.build())
|
||||
.build();
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(setFlag(UV)).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("UV flag");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 16. If the BE bit of the flags in authData is not set, verify that the BS bit is
|
||||
* not set.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenBENotSetAndBSSet() throws Exception {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential(setFlag(BE)).build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("Backup state bit");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 17. If the Relying Party uses the credential’s backup eligibility to inform its
|
||||
* user experience flows and/or policies, evaluate the BE bit of the flags in
|
||||
* authData.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenBEInformsUserExperienceBETrue() {
|
||||
// FIXME: Implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 18. If the Relying Party uses the credential’s backup state to inform its user
|
||||
* experience flows and/or policies, evaluate the BS bit of the flags in authData.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenBSInformsUserExperienceBSTrue() {
|
||||
// FIXME: Search for AuthenticatorDataFlags.BS to implement
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 19. Verify that the "alg" parameter in the credential public key in authData
|
||||
* matches the alg attribute of one of the items in options.pubKeyCredParams.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenAlgDoesNotMatchOptions() {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.pubKeyCredParams(PublicKeyCredentialParameters.RS1)
|
||||
.build();
|
||||
PublicKeyCredential<AuthenticatorAttestationResponse> publicKey = TestPublicKeyCredential
|
||||
.createPublicKeyCredential()
|
||||
.build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
assertThatRuntimeException().isThrownBy(() -> this.rpOperations.registerCredential(registrationRequest))
|
||||
.withMessageContaining("options.pubKeyCredParams");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* 20. Verify that the values of the client extension outputs in
|
||||
* clientExtensionResults and the authenticator extension outputs in the extensions in
|
||||
* authData are as expected, considering the client extension input values that were
|
||||
* given in options.extensions and any specific policy of the Relying Party regarding
|
||||
* unsolicited extensions, i.e., those that were not specified as part of
|
||||
* options.extensions. In the general case, the meaning of "are as expected" is
|
||||
* specific to the Relying Party and which extensions are in use.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenClientExtensionOutputsDoNotMatch() {
|
||||
// FIXME: Implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/webauthn-3/#reg-ceremony-verify-attestation
|
||||
*
|
||||
* 22. Verify that attStmt is a correct attestation statement, conveying a valid
|
||||
* attestation signature, by using the attestation statement format fmt’s verification
|
||||
* procedure given attStmt, authData and hash.
|
||||
*/
|
||||
@Test
|
||||
void registerCredentialWhenFmtNotValid() throws Exception {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
PublicKeyCredential publicKey = TestPublicKeyCredential.createPublicKeyCredential() // setFmt("packed")
|
||||
.build();
|
||||
ImmutableRelyingPartyRegistrationRequest registrationRequest = new ImmutableRelyingPartyRegistrationRequest(
|
||||
options, new RelyingPartyPublicKey(publicKey, this.label));
|
||||
|
||||
// FIXME: Implement this test
|
||||
// assertThatThrownBy(() ->
|
||||
// this.rpOperations.registerCredential(registrationRequest)).hasMessageContaining("Flag
|
||||
// combination is invalid");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCredentialRequestOptionsThenUserVerificationSameAsCreation() {
|
||||
PublicKeyCredentialCreationOptions creationOptions = this.rpOperations.createPublicKeyCredentialCreationOptions(
|
||||
new ImmutablePublicKeyCredentialCreationOptionsRequest(this.user));
|
||||
PublicKeyCredentialRequestOptionsRequest createRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest(
|
||||
this.user);
|
||||
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOperations
|
||||
.createCredentialRequestOptions(createRequest);
|
||||
assertThat(credentialRequestOptions.getUserVerification())
|
||||
.isEqualTo(creationOptions.getAuthenticatorSelection().getUserVerification());
|
||||
}
|
||||
|
||||
private static AuthenticatorAttestationResponse setFlag(byte... flags) throws Exception {
|
||||
AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse();
|
||||
byte[] originalAttestationObjBytes = authAttResponseBldr.build().getAttestationObject().getBytes();
|
||||
ObjectMapper cbor = cbor();
|
||||
AttestationObjectConverter attestationObjectConverter = new AttestationObjectConverter(new ObjectConverter());
|
||||
ObjectNode attObj = (ObjectNode) cbor.readTree(originalAttestationObjBytes);
|
||||
|
||||
byte[] rawAuthData = attObj.get("authData").binaryValue();
|
||||
|
||||
for (byte flag : flags) {
|
||||
rawAuthData[32] ^= flag;
|
||||
}
|
||||
JsonNodeFactory f = JsonNodeFactory.instance;
|
||||
byte[] updatedAttObjBytes = cbor
|
||||
.writeValueAsBytes(attObj.setAll(Map.of("authData", f.binaryNode(rawAuthData))));
|
||||
|
||||
AttestationObject attestationObject = attestationObjectConverter.convert(updatedAttObjBytes);
|
||||
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> authenticatorData = attestationObject
|
||||
.getAuthenticatorData();
|
||||
boolean flagBE = authenticatorData.isFlagBE();
|
||||
boolean flagBS = authenticatorData.isFlagBS();
|
||||
authAttResponseBldr.attestationObject(new Bytes(updatedAttObjBytes));
|
||||
return authAttResponseBldr.build();
|
||||
}
|
||||
|
||||
private static AuthenticatorAttestationResponse setFmt(String fmt) throws Exception {
|
||||
AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponse
|
||||
.createAuthenticatorAttestationResponse();
|
||||
byte[] originalAttestationObjBytes = authAttResponseBldr.build().getAttestationObject().getBytes();
|
||||
ObjectMapper cbor = cbor();
|
||||
ObjectNode attObj = (ObjectNode) cbor.readTree(originalAttestationObjBytes);
|
||||
JsonNodeFactory f = JsonNodeFactory.instance;
|
||||
byte[] updatedAttObjBytes = cbor.writeValueAsBytes(attObj.setAll(Map.of("fmt", f.textNode(fmt))));
|
||||
authAttResponseBldr.attestationObject(new Bytes(updatedAttObjBytes));
|
||||
return authAttResponseBldr.build();
|
||||
}
|
||||
|
||||
private static ObjectMapper cbor() {
|
||||
return new ObjectMapper(new CBORFactory()).setBase64Variant(Base64Variants.MODIFIED_FOR_URL);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.registration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
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.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultWebAuthnRegistrationPageGeneratingFilterTests {
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialUserEntityRepository userEntities;
|
||||
|
||||
@Mock
|
||||
private UserCredentialRepository userCredentials;
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserEntities() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DefaultWebAuthnRegistrationPageGeneratingFilter(null, this.userCredentials))
|
||||
.withMessage("userEntities cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserCredentials() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DefaultWebAuthnRegistrationPageGeneratingFilter(this.userEntities, null))
|
||||
.withMessage("userCredentials cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNotMatchThenNoInteractions() throws Exception {
|
||||
MockMvc mockMvc = mockMvc();
|
||||
mockMvc.perform(get("/not-match"));
|
||||
|
||||
verifyNoInteractions(this.userEntities, this.userCredentials);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterThenCsrfDataAttrsPresent() throws Exception {
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(Bytes.random())
|
||||
.displayName("User")
|
||||
.build();
|
||||
given(this.userEntities.findByUsername(any())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId()))
|
||||
.willReturn(Arrays.asList(TestCredentialRecord.userCredential().build()));
|
||||
String body = bodyAsString(matchingRequest());
|
||||
assertThat(body).contains("setupRegistration({\"X-CSRF-TOKEN\" : \"CSRF_TOKEN\"}");
|
||||
assertThat(body.replaceAll("\\s", "")).contains("""
|
||||
<form class="delete-form no-margin" method="post" action="/webauthn/register/NauGCN7bZ5jEBwThcde51g">
|
||||
<input type="hidden" name="method" value="delete">
|
||||
<input type="hidden" name="_csrf" value="CSRF_TOKEN">
|
||||
<button class="primary small" type="submit">Delete</button>
|
||||
</form>""".replaceAll("\\s", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNullPublicKeyCredentialUserEntityThenNoResults() throws Exception {
|
||||
String body = bodyAsString(matchingRequest());
|
||||
assertThat(body).contains("No Passkeys");
|
||||
verifyNoInteractions(this.userCredentials);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNoCredentialsThenNoResults() throws Exception {
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(Bytes.random())
|
||||
.displayName("User")
|
||||
.build();
|
||||
given(this.userEntities.findByUsername(any())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Collections.emptyList());
|
||||
String body = bodyAsString(matchingRequest());
|
||||
assertThat(body).contains("No Passkeys");
|
||||
verify(this.userCredentials).findByUserId(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenResultsThenDisplayed() throws Exception {
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(Bytes.random())
|
||||
.displayName("User")
|
||||
.build();
|
||||
|
||||
ImmutableCredentialRecord credential = TestCredentialRecord.userCredential()
|
||||
.created(LocalDateTime.of(2024, 9, 17, 10, 10, 42, 999_999_999).toInstant(ZoneOffset.UTC))
|
||||
.lastUsed(LocalDateTime.of(2024, 9, 18, 11, 11, 42, 999_999_999).toInstant(ZoneOffset.UTC))
|
||||
.build();
|
||||
given(this.userEntities.findByUsername(any())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Arrays.asList(credential));
|
||||
String body = bodyAsString(matchingRequest());
|
||||
assertThat(body).isEqualTo(
|
||||
"""
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>WebAuthn Registration</title>
|
||||
<link href="/default-ui.css" rel="stylesheet" />
|
||||
<script type="text/javascript" src="/login/webauthn.js"></script>
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
const ui = {
|
||||
getRegisterButton: function() {
|
||||
return document.getElementById('register')
|
||||
},
|
||||
getSuccess: function() {
|
||||
return document.getElementById('success')
|
||||
},
|
||||
getError: function() {
|
||||
return document.getElementById('error')
|
||||
},
|
||||
getLabelInput: function() {
|
||||
return document.getElementById('label')
|
||||
},
|
||||
getDeleteForms: function() {
|
||||
return Array.from(document.getElementsByClassName("delete-form"))
|
||||
},
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded",() => setupRegistration({"X-CSRF-TOKEN" : "CSRF_TOKEN"}, "", ui));
|
||||
//-->
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h2 class="center">WebAuthn Registration</h2>
|
||||
<form class="default-form" method="post" action="#" onclick="return false">
|
||||
<div id="success" class="alert alert-success" role="alert">Success!</div>
|
||||
<div id="error" class="alert alert-danger" role="alert"></div>
|
||||
<p>
|
||||
<label for="label" class="screenreader">Passkey Label</label>
|
||||
<input type="text" id="label" name="label" placeholder="Passkey Label" required autofocus>
|
||||
</p>
|
||||
<button id="register" class="primary" type="submit">Register</button>
|
||||
</form>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr class="table-header">
|
||||
<th>Label</th>
|
||||
<th>Created</th>
|
||||
<th>Last Used</th>
|
||||
<th>Signature Count</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="v-middle">
|
||||
<td>label</td>
|
||||
<td>2024-09-17T10:10:42Z</td>
|
||||
<td>2024-09-18T11:11:42Z</td>
|
||||
<td class="center">0</td>
|
||||
<td>
|
||||
<form class="delete-form no-margin" method="post" action="/webauthn/register/NauGCN7bZ5jEBwThcde51g">
|
||||
<input type="hidden" name="method" value="delete">
|
||||
<input type="hidden" name="_csrf" value="CSRF_TOKEN">
|
||||
<button class="primary small" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenResultsContainEntitiesThenEncoded() throws Exception {
|
||||
String label = "<script>alert('Hello');</script>";
|
||||
String htmlEncodedLabel = HtmlUtils.htmlEscape(label);
|
||||
assertThat(label).isNotEqualTo(htmlEncodedLabel);
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(Bytes.random())
|
||||
.displayName("User")
|
||||
.build();
|
||||
ImmutableCredentialRecord credential = TestCredentialRecord.userCredential().label(label).build();
|
||||
given(this.userEntities.findByUsername(any())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Arrays.asList(credential));
|
||||
String body = bodyAsString(matchingRequest());
|
||||
assertThat(body).doesNotContain(credential.getLabel());
|
||||
assertThat(body).contains(htmlEncodedLabel);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenContextEmptyThenUrlsEmptyPrefix() throws Exception {
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(Bytes.random())
|
||||
.displayName("User")
|
||||
.build();
|
||||
ImmutableCredentialRecord credential = TestCredentialRecord.userCredential().build();
|
||||
given(this.userEntities.findByUsername(any())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Arrays.asList(credential));
|
||||
String body = bodyAsString(matchingRequest());
|
||||
assertThat(body).contains("<script type=\"text/javascript\" src=\"/login/webauthn.js\"></script>");
|
||||
assertThat(body).contains(
|
||||
"document.addEventListener(\"DOMContentLoaded\",() => setupRegistration({\"X-CSRF-TOKEN\" : \"CSRF_TOKEN\"}, \"\",");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenContextNotEmptyThenUrlsPrefixed() throws Exception {
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(Bytes.random())
|
||||
.displayName("User")
|
||||
.build();
|
||||
ImmutableCredentialRecord credential = TestCredentialRecord.userCredential().build();
|
||||
given(this.userEntities.findByUsername(any())).willReturn(userEntity);
|
||||
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Arrays.asList(credential));
|
||||
String body = bodyAsString(matchingRequest("/foo"));
|
||||
assertThat(body).contains("<script type=\"text/javascript\" src=\"/foo/login/webauthn.js\"></script>");
|
||||
assertThat(body).contains("setupRegistration({\"X-CSRF-TOKEN\" : \"CSRF_TOKEN\"}, \"/foo\",");
|
||||
}
|
||||
|
||||
private String bodyAsString(RequestBuilder request) throws Exception {
|
||||
MockMvc mockMvc = mockMvc();
|
||||
MvcResult result = mockMvc.perform(request).andReturn();
|
||||
MockHttpServletResponse response = result.getResponse();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
|
||||
return response.getContentAsString();
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder matchingRequest() {
|
||||
return matchingRequest("");
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder matchingRequest(String contextPath) {
|
||||
DefaultCsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "CSRF_TOKEN");
|
||||
return get(contextPath + "/webauthn/register").contextPath(contextPath)
|
||||
.requestAttr(CsrfToken.class.getName(), token);
|
||||
}
|
||||
|
||||
private MockMvc mockMvc() {
|
||||
return MockMvcBuilders.standaloneSetup(new Object())
|
||||
.addFilter(new DefaultWebAuthnRegistrationPageGeneratingFilter(this.userEntities, this.userCredentials))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.registration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for
|
||||
* {@link org.springframework.security.web.webauthn.authentication.HttpSessionPublicKeyCredentialRequestOptionsRepository}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
class HttpSessionPublicKeyCredentialCreationOptionsRepositoryTests {
|
||||
|
||||
private HttpSessionPublicKeyCredentialCreationOptionsRepository repository = new HttpSessionPublicKeyCredentialCreationOptionsRepository();
|
||||
|
||||
private MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
@Test
|
||||
void integrationTests() {
|
||||
PublicKeyCredentialCreationOptions expected = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
this.repository.save(this.request, this.response, expected);
|
||||
Object attrValue = this.request.getSession()
|
||||
.getAttribute(HttpSessionPublicKeyCredentialCreationOptionsRepository.DEFAULT_ATTR_NAME);
|
||||
assertThat(attrValue).isEqualTo(expected);
|
||||
PublicKeyCredentialCreationOptions loadOptions = this.repository.load(this.request);
|
||||
assertThat(loadOptions).isEqualTo(expected);
|
||||
|
||||
this.repository.save(this.request, this.response, null);
|
||||
|
||||
Object attrValueAfterRemoval = this.request.getSession()
|
||||
.getAttribute(HttpSessionPublicKeyCredentialCreationOptionsRepository.DEFAULT_ATTR_NAME);
|
||||
assertThat(attrValueAfterRemoval).isNull();
|
||||
assertThat(this.request.getSession().getAttributeNames())
|
||||
.doesNotHaveToString(HttpSessionPublicKeyCredentialCreationOptionsRepository.DEFAULT_ATTR_NAME);
|
||||
PublicKeyCredentialCreationOptions loadOptionsAfterRemoval = this.repository.load(this.request);
|
||||
assertThat(loadOptionsAfterRemoval).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenNullSessionThenDoesNotCreate() {
|
||||
PublicKeyCredentialCreationOptions options = this.repository.load(this.request);
|
||||
assertThat(options).isNull();
|
||||
assertThat(this.request.getSession(false)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenSetAttrThenCustomAttr() {
|
||||
PublicKeyCredentialCreationOptions expected = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
String customAttr = "custom-attr";
|
||||
this.repository.setAttrName(customAttr);
|
||||
this.repository.save(this.request, this.response, expected);
|
||||
assertThat(this.request.getSession().getAttribute(customAttr)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenSetAttrThenCustomAttr() {
|
||||
PublicKeyCredentialCreationOptions expected = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
String customAttr = "custom-attr";
|
||||
this.repository.setAttrName(customAttr);
|
||||
this.request.getSession().setAttribute(customAttr, expected);
|
||||
PublicKeyCredentialCreationOptions actual = this.repository.load(this.request);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.registration;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
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.verifyNoInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for {@link PublicKeyCredentialCreationOptionsFilter}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PublicKeyCredentialCreationOptionsFilterTests {
|
||||
|
||||
private static String REGISTER_OPTONS_URL = "/webauthn/register/options";
|
||||
|
||||
@Mock
|
||||
private WebAuthnRelyingPartyOperations rpOperations;
|
||||
|
||||
@AfterEach
|
||||
void clear() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenRpOperationsIsNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new PublicKeyCredentialCreationOptionsFilter(null))
|
||||
.withMessage("rpOperations cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenWrongUrlThenNoInteractions() throws Exception {
|
||||
MockMvc mockMvc = mockMvc();
|
||||
mockMvc.perform(post("/foo"));
|
||||
verifyNoInteractions(this.rpOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNotAuthenticatedThenNoInvocations() throws Exception {
|
||||
MockMvc mockMvc = mockMvc();
|
||||
MockHttpServletResponse response = mockMvc.perform(post(REGISTER_OPTONS_URL)).andReturn().getResponse();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenAnonymousThenNoInvocations() throws Exception {
|
||||
AnonymousAuthenticationToken anonymous = new AnonymousAuthenticationToken("key", "anonymousUser",
|
||||
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
SecurityContextImpl context = new SecurityContextImpl(anonymous);
|
||||
SecurityContextHolder.setContext(context);
|
||||
MockMvc mockMvc = mockMvc();
|
||||
MockHttpServletResponse response = mockMvc.perform(post(REGISTER_OPTONS_URL)).andReturn().getResponse();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenGetThenNoInteractions() throws Exception {
|
||||
MockMvc mockMvc = mockMvc();
|
||||
mockMvc.perform(get(REGISTER_OPTONS_URL));
|
||||
verifyNoInteractions(this.rpOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNoCredentials() throws Exception {
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
given(this.rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options);
|
||||
MockMvc mockMvc = mockMvc();
|
||||
mockMvc.perform(matchingRequest())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().json("""
|
||||
{
|
||||
"rp": {
|
||||
"name": "SimpleWebAuthn Example",
|
||||
"id": "example.localhost"
|
||||
},
|
||||
"user": {
|
||||
"name": "user@example.localhost",
|
||||
"id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
|
||||
"displayName": "user@example.localhost"
|
||||
},
|
||||
"challenge": "q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc",
|
||||
"pubKeyCredParams": [
|
||||
{
|
||||
"type": "public-key",
|
||||
"alg": -8
|
||||
},
|
||||
{
|
||||
"type": "public-key",
|
||||
"alg": -7
|
||||
},
|
||||
{
|
||||
"type": "public-key",
|
||||
"alg": -257
|
||||
}
|
||||
],
|
||||
"timeout": 300000,
|
||||
"excludeCredentials": [],
|
||||
"authenticatorSelection": {
|
||||
"residentKey": "required",
|
||||
"userVerification": "preferred"
|
||||
},
|
||||
"attestation": "direct",
|
||||
"extensions": {
|
||||
"credProps": true
|
||||
}
|
||||
}
|
||||
"""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenExcludeCredentialsThenIncludedInResponse() throws Exception {
|
||||
PublicKeyCredentialDescriptor credentialDescriptor = PublicKeyCredentialDescriptor.builder()
|
||||
.transports(AuthenticatorTransport.HYBRID)
|
||||
.id(Bytes.fromBase64("ChfoCM8CJA_wwUGDdzdtuw"))
|
||||
.build();
|
||||
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.excludeCredentials(Arrays.asList(credentialDescriptor))
|
||||
.build();
|
||||
given(this.rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options);
|
||||
MockMvc mockMvc = mockMvc();
|
||||
mockMvc.perform(matchingRequest())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().json("""
|
||||
{
|
||||
"excludeCredentials": [
|
||||
{
|
||||
"type": "public-key",
|
||||
"id": "ChfoCM8CJA_wwUGDdzdtuw",
|
||||
"transports": [
|
||||
"hybrid"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""));
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder matchingRequest() {
|
||||
TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
|
||||
SecurityContextHolder.setContext(new SecurityContextImpl(user));
|
||||
return post(REGISTER_OPTONS_URL);
|
||||
}
|
||||
|
||||
private MockMvc mockMvc() {
|
||||
return MockMvcBuilders.standaloneSetup(new Object())
|
||||
.addFilter(new PublicKeyCredentialCreationOptionsFilter(this.rpOperations))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.registration;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebAuthnRegistrationFilter}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WebAuthnRegistrationFilterTests {
|
||||
|
||||
@Mock
|
||||
private UserCredentialRepository userCredentials;
|
||||
|
||||
@Mock
|
||||
private WebAuthnRelyingPartyOperations operations;
|
||||
|
||||
@Mock
|
||||
private GenericHttpMessageConverter<Object> converter;
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository;
|
||||
|
||||
@Mock
|
||||
private FilterChain chain;
|
||||
|
||||
private MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
private static final String REGISTRATION_REQUEST_BODY = """
|
||||
{
|
||||
"publicKey": {
|
||||
"credential": {
|
||||
"id": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||
"response": {
|
||||
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEHWBexBp0RRSF5KV4vV1NsKlAQIDJiABIVggQjmrekPGzyqtoKK9HPUH-8Z2FLpoqkklFpFPQVICQ3IiWCD6I9Jvmor685fOZOyGXqUd87tXfvJk8rxj9OhuZvUALA",
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSl9RTi10SFJYRWVKYjlNcUNrWmFPLUdOVmlibXpGVGVWMk43Z0ptQUdrQSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||
"transports": [
|
||||
"internal",
|
||||
"hybrid"
|
||||
]
|
||||
},
|
||||
"type": "public-key",
|
||||
"clientExtensionResults": {},
|
||||
"authenticatorAttachment": "platform"
|
||||
},
|
||||
"label": "1password"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private WebAuthnRegistrationFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.filter = new WebAuthnRegistrationFilter(this.userCredentials, this.operations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserCredentials() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new WebAuthnRegistrationFilter(null, this.operations));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullOperations() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new WebAuthnRegistrationFilter(this.userCredentials, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenRegisterUrlDoesNotMatchThenChainContinues() throws Exception {
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
this.filter.setConverter(this.converter);
|
||||
this.filter.setCreationOptionsRepository(this.creationOptionsRepository);
|
||||
this.filter.doFilter(post("/nomatch").buildRequest(new MockServletContext()), response, this.chain);
|
||||
verifyNoInteractions(this.converter, this.creationOptionsRepository, response);
|
||||
verify(this.chain).doFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenRegisterMethodDoesNotMatchThenChainContinues() throws Exception {
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
this.filter.setConverter(this.converter);
|
||||
this.filter.setCreationOptionsRepository(this.creationOptionsRepository);
|
||||
this.filter.doFilter(
|
||||
get(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL).buildRequest(new MockServletContext()),
|
||||
response, this.chain);
|
||||
verifyNoInteractions(this.converter, this.creationOptionsRepository, response);
|
||||
verify(this.chain).doFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenRegisterNoBodyThenBadRequest() throws Exception {
|
||||
this.filter.doFilter(registerCredentialRequest(""), this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenInvalidJsonThenBadRequest() throws Exception {
|
||||
this.filter.doFilter(registerCredentialRequest("{"), this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenRegisterOptionsNullThenBadRequest() throws Exception {
|
||||
this.filter.setCreationOptionsRepository(this.creationOptionsRepository);
|
||||
MockHttpServletRequest request = registerCredentialRequest(REGISTRATION_REQUEST_BODY);
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenRegisterSuccessThenOk() throws Exception {
|
||||
this.filter.setCreationOptionsRepository(this.creationOptionsRepository);
|
||||
PublicKeyCredentialCreationOptions creationOptions = TestPublicKeyCredentialCreationOptions
|
||||
.createPublicKeyCredentialCreationOptions()
|
||||
.build();
|
||||
given(this.creationOptionsRepository.load(any())).willReturn(creationOptions);
|
||||
ImmutableCredentialRecord userCredential = TestCredentialRecord.userCredential().build();
|
||||
given(this.operations.registerCredential(any())).willReturn(userCredential);
|
||||
MockHttpServletRequest request = registerCredentialRequest(REGISTRATION_REQUEST_BODY);
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
String actualBody = this.response.getContentAsString();
|
||||
String expectedBody = """
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
""";
|
||||
JSONAssert.assertEquals(expectedBody, actualBody, false);
|
||||
verify(this.creationOptionsRepository).save(any(), any(), eq(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
|
||||
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());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest registerCredentialRequest(String body) {
|
||||
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
|
||||
.content(body)
|
||||
.buildRequest(new MockServletContext());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.registration;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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.CredentialPropertiesOutput;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
|
||||
import org.springframework.security.web.webauthn.jackson.PublicKeyCredentialJson;
|
||||
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
|
||||
import org.springframework.security.web.webauthn.management.RelyingPartyPublicKey;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @since 6.4
|
||||
*/
|
||||
class WebAuthnRegistrationRequestJacksonTests {
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModule(new WebauthnJackson2Module());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readRelyingPartyRequest() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"publicKey": {
|
||||
"label": "Cell Phone",
|
||||
"credential": %s
|
||||
}
|
||||
}
|
||||
""".formatted(PublicKeyCredentialJson.PUBLIC_KEY_JSON);
|
||||
WebAuthnRegistrationFilter.WebAuthnRegistrationRequest registrationRequest = this.mapper.readValue(json,
|
||||
WebAuthnRegistrationFilter.WebAuthnRegistrationRequest.class);
|
||||
|
||||
ImmutableAuthenticationExtensionsClientOutputs clientExtensionResults = new ImmutableAuthenticationExtensionsClientOutputs(
|
||||
new CredentialPropertiesOutput(false));
|
||||
|
||||
PublicKeyCredential<AuthenticatorAttestationResponse> credential = PublicKeyCredential.builder()
|
||||
.id("AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM")
|
||||
.rawId(Bytes
|
||||
.fromBase64("AX6nVVERrH6opMafUGn3Z9EyNEy6cftfBKV_2YxYl1jdW8CSJxMKGXFV3bnrKTiMSJeInkG7C6B2lPt8E5i3KaM"))
|
||||
.response(AuthenticatorAttestationResponse.builder()
|
||||
.attestationObject(Bytes.fromBase64(
|
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQF-p1VREax-qKTGn1Bp92fRMjRMunH7XwSlf9mMWJdY3VvAkicTChlxVd256yk4jEiXiJ5BuwugdpT7fBOYtymjpQECAyYgASFYIJK-2epPEw0ujHN-gvVp2Hp3ef8CzU3zqwO5ylx8L2OsIlggK5x5OlTGEPxLS-85TAABum4aqVK4CSWJ7LYDdkjuBLk"))
|
||||
.clientDataJSON(Bytes.fromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSUJRbnVZMVowSzFIcUJvRldDcDJ4bEpsOC1vcV9hRklYenlUX0YwLTBHVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"))
|
||||
.transports(AuthenticatorTransport.HYBRID, AuthenticatorTransport.INTERNAL)
|
||||
.build())
|
||||
.type(PublicKeyCredentialType.PUBLIC_KEY)
|
||||
.clientExtensionResults(clientExtensionResults)
|
||||
.authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
|
||||
.build();
|
||||
|
||||
WebAuthnRegistrationFilter.WebAuthnRegistrationRequest expected = new WebAuthnRegistrationFilter.WebAuthnRegistrationRequest();
|
||||
expected.setPublicKey(new RelyingPartyPublicKey(credential, "Cell Phone"));
|
||||
assertThat(registrationRequest).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user