Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d99a7fb14 | |||
| 0400e29df1 | |||
| 7ad28772bc | |||
| 2678925c21 | |||
| 87472c9ab4 | |||
| ea036702fc | |||
| 09805317e6 | |||
| 4ab933803f | |||
| d2b1cb572f | |||
| 3e936ebe75 | |||
| ecc2775796 | |||
| cf8e3ee2ab | |||
| 12ba56bbf2 | |||
| 2f8638d867 | |||
| 17aad2cea3 | |||
| 9184ad3ad5 | |||
| 70076188af | |||
| 57c9b1365c | |||
| 1c3d28f14d | |||
| aaadb43ef8 | |||
| a6c1a02afa | |||
| baebd04df7 | |||
| d0166004aa | |||
| 9f96fbcda0 | |||
| ccffb48fd1 | |||
| d0fcdebe88 | |||
| 26991bbe5f | |||
| e4106ecf68 | |||
| 7cb57ab940 | |||
| b130e728b7 | |||
| e6db56ab4f | |||
| af47cc2abe | |||
| f997e22d9d | |||
| b7fb2892ed | |||
| c85cb2d1ef | |||
| bf26dd9b33 | |||
| ff908c4d7c | |||
| 521f533fc4 | |||
| ee49c18ce2 | |||
| f0afca7610 | |||
| 8b0689cbb8 | |||
| 28e158d1cb | |||
| 36f1f2ca4f | |||
| 46b6744b42 |
+97
-10
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebDriverException;
|
||||
@@ -55,6 +56,7 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
@@ -67,7 +69,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
*/
|
||||
@org.junit.jupiter.api.Disabled
|
||||
@Disabled
|
||||
class WebAuthnWebDriverTests {
|
||||
|
||||
private String baseUrl;
|
||||
@@ -82,6 +84,8 @@ class WebAuthnWebDriverTests {
|
||||
|
||||
private static final String PASSWORD = "password";
|
||||
|
||||
private String authenticatorId = null;
|
||||
|
||||
@BeforeAll
|
||||
static void startChromeDriverService() throws Exception {
|
||||
driverService = new ChromeDriverService.Builder().usingAnyFreePort().build();
|
||||
@@ -144,7 +148,7 @@ class WebAuthnWebDriverTests {
|
||||
@Test
|
||||
void loginWhenNoValidAuthenticatorCredentialsThenRejects() {
|
||||
createVirtualAuthenticator(true);
|
||||
this.driver.get(this.baseUrl);
|
||||
this.getAndWait("/", "/login");
|
||||
this.driver.findElement(signinWithPasskeyButton()).click();
|
||||
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?error"));
|
||||
}
|
||||
@@ -153,7 +157,7 @@ class WebAuthnWebDriverTests {
|
||||
void registerWhenNoLabelThenRejects() {
|
||||
login();
|
||||
|
||||
this.driver.get(this.baseUrl + "/webauthn/register");
|
||||
this.getAndWait("/webauthn/register");
|
||||
|
||||
this.driver.findElement(registerPasskeyButton()).click();
|
||||
assertHasAlertStartingWith("error", "Error: Passkey Label is required");
|
||||
@@ -163,7 +167,7 @@ class WebAuthnWebDriverTests {
|
||||
void registerWhenAuthenticatorNoUserVerificationThenRejects() {
|
||||
createVirtualAuthenticator(false);
|
||||
login();
|
||||
this.driver.get(this.baseUrl + "/webauthn/register");
|
||||
this.getAndWait("/webauthn/register");
|
||||
this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator");
|
||||
this.driver.findElement(registerPasskeyButton()).click();
|
||||
|
||||
@@ -178,7 +182,8 @@ class WebAuthnWebDriverTests {
|
||||
* <li>Step 1: Log in with username / password</li>
|
||||
* <li>Step 2: Register a credential from the virtual authenticator</li>
|
||||
* <li>Step 3: Log out</li>
|
||||
* <li>Step 4: Log in with the authenticator</li>
|
||||
* <li>Step 4: Log in with the authenticator (no allowCredentials)</li>
|
||||
* <li>Step 5: Log in again with the same authenticator (with allowCredentials)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
@@ -190,7 +195,7 @@ class WebAuthnWebDriverTests {
|
||||
login();
|
||||
|
||||
// Step 2: register a credential from the virtual authenticator
|
||||
this.driver.get(this.baseUrl + "/webauthn/register");
|
||||
this.getAndWait("/webauthn/register");
|
||||
this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator");
|
||||
this.driver.findElement(registerPasskeyButton()).click();
|
||||
|
||||
@@ -212,9 +217,58 @@ class WebAuthnWebDriverTests {
|
||||
logout();
|
||||
|
||||
// Step 4: log in with the virtual authenticator
|
||||
this.driver.get(this.baseUrl + "/webauthn/register");
|
||||
this.getAndWait("/webauthn/register", "/login");
|
||||
this.driver.findElement(signinWithPasskeyButton()).click();
|
||||
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?continue"));
|
||||
|
||||
// Step 5: authenticate while being already logged in
|
||||
// This simulates some use-cases with MFA. Since the user is already logged in,
|
||||
// the "allowCredentials" property is populated
|
||||
this.getAndWait("/login");
|
||||
this.driver.findElement(signinWithPasskeyButton()).click();
|
||||
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerWhenAuthenticatorAlreadyRegisteredThenRejects() {
|
||||
createVirtualAuthenticator(true);
|
||||
login();
|
||||
registerAuthenticator("Virtual authenticator");
|
||||
|
||||
// Cannot re-register the same authenticator because excludeCredentials
|
||||
// is not empty and contains the given authenticator
|
||||
this.driver.findElement(passkeyLabel()).sendKeys("Same authenticator");
|
||||
this.driver.findElement(registerPasskeyButton()).click();
|
||||
|
||||
await(() -> assertHasAlertStartingWith("error", "Registration failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerSecondAuthenticatorThenSucceeds() {
|
||||
createVirtualAuthenticator(true);
|
||||
login();
|
||||
|
||||
registerAuthenticator("Virtual authenticator");
|
||||
this.getAndWait("/webauthn/register");
|
||||
List<WebElement> passkeyRows = this.driver.findElements(passkeyTableRows());
|
||||
assertThat(passkeyRows).hasSize(1)
|
||||
.first()
|
||||
.extracting((row) -> row.findElement(firstCell()))
|
||||
.extracting(WebElement::getText)
|
||||
.isEqualTo("Virtual authenticator");
|
||||
|
||||
// Create second authenticator and register
|
||||
removeAuthenticator();
|
||||
createVirtualAuthenticator(true);
|
||||
registerAuthenticator("Second virtual authenticator");
|
||||
|
||||
this.getAndWait("/webauthn/register");
|
||||
|
||||
passkeyRows = this.driver.findElements(passkeyTableRows());
|
||||
assertThat(passkeyRows).hasSize(2)
|
||||
.extracting((row) -> row.findElement(firstCell()))
|
||||
.extracting(WebElement::getText)
|
||||
.contains("Second virtual authenticator");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,11 +285,14 @@ class WebAuthnWebDriverTests {
|
||||
* "https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/">https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/</a>
|
||||
*/
|
||||
private void createVirtualAuthenticator(boolean userIsVerified) {
|
||||
if (StringUtils.hasText(this.authenticatorId)) {
|
||||
throw new IllegalStateException("Authenticator already exists, please remove it before re-creating one");
|
||||
}
|
||||
HasCdp cdpDriver = (HasCdp) this.driver;
|
||||
cdpDriver.executeCdpCommand("WebAuthn.enable", Map.of("enableUI", false));
|
||||
// this.driver.addVirtualAuthenticator(createVirtualAuthenticatorOptions());
|
||||
//@formatter:off
|
||||
cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator",
|
||||
Map<String, Object> cmdResponse = cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator",
|
||||
Map.of(
|
||||
"options",
|
||||
Map.of(
|
||||
@@ -248,21 +305,38 @@ class WebAuthnWebDriverTests {
|
||||
)
|
||||
));
|
||||
//@formatter:on
|
||||
this.authenticatorId = cmdResponse.get("authenticatorId").toString();
|
||||
}
|
||||
|
||||
private void removeAuthenticator() {
|
||||
HasCdp cdpDriver = (HasCdp) this.driver;
|
||||
cdpDriver.executeCdpCommand("WebAuthn.removeVirtualAuthenticator",
|
||||
Map.of("authenticatorId", this.authenticatorId));
|
||||
this.authenticatorId = null;
|
||||
}
|
||||
|
||||
private void login() {
|
||||
this.driver.get(this.baseUrl);
|
||||
this.getAndWait("/", "/login");
|
||||
this.driver.findElement(usernameField()).sendKeys(USERNAME);
|
||||
this.driver.findElement(passwordField()).sendKeys(PASSWORD);
|
||||
this.driver.findElement(signinWithUsernamePasswordButton()).click();
|
||||
// Ensure login has completed
|
||||
await(() -> assertThat(this.driver.getCurrentUrl()).doesNotContain("/login"));
|
||||
}
|
||||
|
||||
private void logout() {
|
||||
this.driver.get(this.baseUrl + "/logout");
|
||||
this.getAndWait("/logout");
|
||||
this.driver.findElement(logoutButton()).click();
|
||||
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?logout"));
|
||||
}
|
||||
|
||||
private void registerAuthenticator(String passkeyName) {
|
||||
this.getAndWait("/webauthn/register");
|
||||
this.driver.findElement(passkeyLabel()).sendKeys(passkeyName);
|
||||
this.driver.findElement(registerPasskeyButton()).click();
|
||||
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?success"));
|
||||
}
|
||||
|
||||
private AbstractStringAssert<?> assertHasAlertStartingWith(String alertType, String alertMessage) {
|
||||
WebElement alert = this.driver.findElement(new By.ById(alertType));
|
||||
assertThat(alert.isDisplayed())
|
||||
@@ -289,6 +363,15 @@ class WebAuthnWebDriverTests {
|
||||
});
|
||||
}
|
||||
|
||||
private void getAndWait(String endpoint) {
|
||||
this.getAndWait(endpoint, endpoint);
|
||||
}
|
||||
|
||||
private void getAndWait(String endpoint, String redirectUrl) {
|
||||
this.driver.get(this.baseUrl + endpoint);
|
||||
this.await(() -> assertThat(this.driver.getCurrentUrl()).endsWith(redirectUrl));
|
||||
}
|
||||
|
||||
private static By.ById passkeyLabel() {
|
||||
return new By.ById("label");
|
||||
}
|
||||
@@ -325,6 +408,10 @@ class WebAuthnWebDriverTests {
|
||||
return new By.ByCssSelector("button");
|
||||
}
|
||||
|
||||
private static By.ByCssSelector deletePasskeyButton() {
|
||||
return new By.ByCssSelector("table > tbody > tr > button");
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration for WebAuthN tests. It accesses the Server's current port, so we
|
||||
* can configurer WebAuthnConfigurer#allowedOrigin
|
||||
|
||||
@@ -108,6 +108,34 @@ spring:
|
||||
require-authorization-consent: true
|
||||
----
|
||||
|
||||
If you want to customize the default `HttpSecurity` configuration, you may override Spring Boot's auto-configuration with the following example:
|
||||
|
||||
[[oauth2AuthorizationServer-minimal-sample-gettingstarted]]
|
||||
.SecurityConfig.java
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oauth2AuthorizationServer((authorizationServer) ->
|
||||
authorizationServer
|
||||
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
TIP: Beyond the Getting Started experience, most users will want to customize the default configuration. The xref:servlet/oauth2/authorization-server/getting-started.adoc#oauth2AuthorizationServer-defining-required-components[next section] demonstrates providing all of the necessary beans yourself.
|
||||
|
||||
[[oauth2AuthorizationServer-defining-required-components]]
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=4.0.0-SNAPSHOT
|
||||
version=7.0.0-RC3
|
||||
version=7.0.0
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -8,11 +8,11 @@ org-apache-maven-resolver = "1.9.24"
|
||||
org-aspectj = "1.9.24"
|
||||
org-bouncycastle = "1.80"
|
||||
org-eclipse-jetty = "11.0.26"
|
||||
org-jetbrains-kotlin = "2.2.20"
|
||||
org-jetbrains-kotlin = "2.2.21"
|
||||
org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml5 = "5.1.6"
|
||||
org-springframework = "7.0.0-RC1"
|
||||
org-springframework = "7.0.0"
|
||||
com-password4j = "1.8.4"
|
||||
|
||||
[libraries]
|
||||
@@ -30,7 +30,7 @@ commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.3"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.12"
|
||||
io-mockk = "io.mockk:mockk:1.14.6"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.0-RC1"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.0"
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
@@ -68,7 +68,7 @@ org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.0.10.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20"
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21"
|
||||
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
|
||||
org-junit-junit-bom = "org.junit:junit-bom:6.0.0"
|
||||
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
|
||||
@@ -81,8 +81,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.0-RC1"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.0-RC1"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.0"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.0"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.1"
|
||||
|
||||
+11
-2
@@ -95,7 +95,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
|
||||
|
||||
private final PublicKeyCredentialRpEntity rp;
|
||||
|
||||
private final ObjectConverter objectConverter = new ObjectConverter();
|
||||
private ObjectConverter objectConverter = new ObjectConverter();
|
||||
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
@@ -137,6 +137,15 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
|
||||
this.webAuthnManager = webAuthnManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ObjectConverter} to use.
|
||||
* @param objectConverter the {@link ObjectConverter} to use. Cannot be null.
|
||||
*/
|
||||
void setObjectConverter(ObjectConverter objectConverter) {
|
||||
Assert.notNull(objectConverter, "objectConverter cannot be null");
|
||||
this.objectConverter = objectConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link Consumer} used to customize the
|
||||
* {@link PublicKeyCredentialCreationOptionsBuilder} for
|
||||
@@ -390,7 +399,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
|
||||
.getUserVerification() == UserVerificationRequirement.REQUIRED;
|
||||
|
||||
com.webauthn4j.data.AuthenticationRequest authenticationRequest = new com.webauthn4j.data.AuthenticationRequest(
|
||||
request.getPublicKey().getId().getBytes(), assertionResponse.getAuthenticatorData().getBytes(),
|
||||
request.getPublicKey().getRawId().getBytes(), assertionResponse.getAuthenticatorData().getBytes(),
|
||||
assertionResponse.getClientDataJSON().getBytes(), assertionResponse.getSignature().getBytes());
|
||||
|
||||
// CollectedClientData and ExtensionsClientOutputs is registration data, and can
|
||||
|
||||
+55
@@ -27,15 +27,20 @@ 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.WebAuthnManager;
|
||||
import com.webauthn4j.converter.AttestationObjectConverter;
|
||||
import com.webauthn4j.converter.util.ObjectConverter;
|
||||
import com.webauthn4j.data.AuthenticationData;
|
||||
import com.webauthn4j.data.AuthenticationRequest;
|
||||
import com.webauthn4j.data.attestation.AttestationObject;
|
||||
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@@ -44,12 +49,14 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||
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.AuthenticatorTransport;
|
||||
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.PublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
|
||||
@@ -57,9 +64,11 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialParamete
|
||||
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.TestAuthenticationAssertionResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentials;
|
||||
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
|
||||
@@ -67,7 +76,9 @@ 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.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -587,6 +598,50 @@ class Webauthn4jRelyingPartyOperationsTests {
|
||||
.containsExactly(credentialRecord.getCredentialId());
|
||||
}
|
||||
|
||||
// gh-18158
|
||||
@Test
|
||||
void authenticateThenWa4jRequestCredentialIdIsRawIdBytes() throws Exception {
|
||||
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create().build();
|
||||
AuthenticatorAssertionResponse response = TestAuthenticationAssertionResponses
|
||||
.createAuthenticatorAssertionResponse()
|
||||
.build();
|
||||
PublicKeyCredential<AuthenticatorAssertionResponse> credentials = TestPublicKeyCredentials
|
||||
.createPublicKeyCredential(response)
|
||||
.build();
|
||||
RelyingPartyAuthenticationRequest request = new RelyingPartyAuthenticationRequest(options, credentials);
|
||||
PublicKeyCredential<AuthenticatorAssertionResponse> publicKey = request.getPublicKey();
|
||||
|
||||
ImmutableCredentialRecord credentialRecord = TestCredentialRecords.fullUserCredential().build();
|
||||
given(this.userCredentials.findByCredentialId(publicKey.getRawId())).willReturn(credentialRecord);
|
||||
ObjectMapper json = mock(ObjectMapper.class);
|
||||
ObjectMapper cbor = mock(ObjectMapper.class);
|
||||
given(cbor.getFactory()).willReturn(mock(CBORFactory.class));
|
||||
AttestationObject attestationObject = mock(AttestationObject.class);
|
||||
AuthenticatorData wa4jAuthData = mock(AuthenticatorData.class);
|
||||
given(attestationObject.getAuthenticatorData()).willReturn(wa4jAuthData);
|
||||
given(wa4jAuthData.getAttestedCredentialData()).willReturn(mock(AttestedCredentialData.class));
|
||||
given(cbor.readValue(credentialRecord.getAttestationObject().getBytes(), AttestationObject.class))
|
||||
.willReturn(attestationObject);
|
||||
this.rpOperations.setObjectConverter(new ObjectConverter(json, cbor));
|
||||
|
||||
WebAuthnManager manager = mock(WebAuthnManager.class);
|
||||
ArgumentCaptor<AuthenticationRequest> wa4jRequest = ArgumentCaptor.forClass(AuthenticationRequest.class);
|
||||
AuthenticationData wa4jData = mock(AuthenticationData.class);
|
||||
given(wa4jData.getAuthenticatorData()).willReturn(mock(AuthenticatorData.class));
|
||||
given(manager.verify(wa4jRequest.capture(), any())).willReturn(wa4jData);
|
||||
given(this.userEntities.findById(any())).willReturn(TestPublicKeyCredentialUserEntities.userEntity().build());
|
||||
this.rpOperations.setWebAuthnManager(manager);
|
||||
|
||||
this.rpOperations.authenticate(request);
|
||||
|
||||
// this ensures that our next assertion is valid (we want the rawId bytes, not the
|
||||
// id bytes to be used)
|
||||
assertThat(publicKey.getRawId().getBytes()).isNotEqualTo(publicKey.getId().getBytes());
|
||||
// ensure that the raw id bytes are passed into webauthn4j (not the id bytes which
|
||||
// are base64 encoded)
|
||||
assertThat(wa4jRequest.getValue().getCredentialId()).isEqualTo(publicKey.getRawId().getBytes());
|
||||
}
|
||||
|
||||
private static AuthenticatorAttestationResponse setFlag(byte... flags) throws Exception {
|
||||
AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponses
|
||||
.createAuthenticatorAttestationResponse();
|
||||
|
||||
Reference in New Issue
Block a user