[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-37 - Lookup user credentials from user handle expects user handle
Phil Smart
philip.smart at jisc.ac.uk
Fri Feb 14 09:18:02 UTC 2025
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-webauthn.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-webauthn.git;a=commit;h=4b77dcf3bbcd7960259bcd7ed414169f465b3a8d
The following commit(s) were added to refs/heads/main by this push:
new 4b77dcf JWEBAUTHN-37 - Lookup user credentials from user handle expects user handle
4b77dcf is described below
commit 4b77dcf3bbcd7960259bcd7ed414169f465b3a8d
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Feb 14 09:17:58 2025 +0000
JWEBAUTHN-37 - Lookup user credentials from user handle expects user
handle
- The lookup action functions correctly if the user is already
identified and has credentials in the context
- Also, streamline the actions (revert the previous commit to seperate
them).
- Add more tests
https://shibboleth.atlassian.net/browse/JWEBAUTHN-37
---
.../impl/CheckUserHandleAgainstUsername.java | 118 ---------------------
.../CheckUserHandleExistsIfNoAllowCredentials.java | 22 +++-
.../LookupRegisteredCredentialsFromUserHandle.java | 53 +++++++--
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 5 -
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 49 +--------
.../authn/webauthn/flow/TestPasswordlessFlow.java | 83 ++++++++++++++-
...dlessFlowTriggerOnNoUserHandleCredentials.java} | 95 +++++------------
.../authn/webauthn/flow/TestSecondFactorFlow.java | 50 +++++++++
.../flow/TestSecondFactorFlowWithFilterPolicy.java | 2 +-
.../flow/TestUsernameslessFlowWithPolicy.java | 4 +-
...NoCredentialsApplicationContextInitializer.java | 50 +++++++++
...kupRegisteredCredentialsFromUserHandleTest.java | 74 ++++++++++++-
12 files changed, 352 insertions(+), 253 deletions(-)
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleAgainstUsername.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleAgainstUsername.java
deleted file mode 100644
index e05e00e..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleAgainstUsername.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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
- *
- * http://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 net.shibboleth.idp.plugin.authn.webauthn.impl;
-
-import java.util.Optional;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialRepository;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.primitive.LoggerFactory;
-
-/**
- * An action that checks if the userHandle in the authentication assertion refers to the same user as in the WebAuthn
- * context. Noting, under some circumstances e.g. a passwordless flow where the user and their credentials have
- * already been identified, the authenticator does not need to provide a userHandle, in which case this action does
- * nothing.
- *
- *
- * @event {EventIds#INVALID_PROFILE_CTX}
- * @event {AuthnEventIds#INVALID_CREDENTIALS}
- * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
- * @post a non-proceed event is signalled if the username found for the userHandle does not match that in the context
- *
- * @since 1.1.0
- */
-public class CheckUserHandleAgainstUsername extends AbstractWebAuthnAction<WebAuthnAuthenticationContext> {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(CheckUserHandleAgainstUsername.class);
-
- /** The credential repository to use.*/
- @NonnullAfterInit private WebAuthnCredentialRepository repository;
-
- /** {@inheritDoc} */
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
- repository = getCredentialRepository();
- if (repository == null) {
- throw new ComponentInitializationException("Credential repository can not be null");
- }
- }
-
- /**
- * Constructor.
- */
- protected CheckUserHandleAgainstUsername() {
- super(new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
- compose(new ChildContextLookup<>(AuthenticationContext.class)));
- }
-
- @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
-
-
- final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion =
- context.getPublicKeyCredentialAssertionResponse();
- if (assertion == null) {
- log.error("{} Unable to find Assertion in WebAuthn authentication context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return;
- }
- final String username = context.getUsername();
-
- final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
- if (userHandle.isEmpty()) {
- log.trace("{} User could not be found, the authenticator did not supply a userHandle", getLogPrefix());
- } else {
- final Optional<String> potentialUsername = repository.getUsernameForUserHandle(userHandle.get());
-
- if (potentialUsername.isEmpty()) {
- log.trace("{} User could not be found from the supplied userHandle, no registered credentials",
- getLogPrefix());
- } else if (username != null && !potentialUsername.get().equals(username)){
-
- log.trace("{} Username '{}' found from the userHandle was not the same as in the authentication "
- + "context '{}'",getLogPrefix(), potentialUsername.get(), username);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
- return;
-
- } else {
- log.trace("{} Username '{}' found from the userHandle matched that in the authentication context",
- getLogPrefix(), potentialUsername.get());
- }
- }
-
- }
-
-
-}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java
index 2bb2530..676185f 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java
@@ -42,6 +42,8 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* in the request was empty. If they are empty, the userHandle must be returned, if they are not empty the userHandle
* may be returned in the response.
*
+ * <p>This generally protects against bad behaviour from authenticators for the rest of the flow.</p>
+ *
* @event {EventIds#INVALID_PROFILE_CTX}
* @event {AuthnEventIds#INVALID_CREDENTIALS}
* @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
@@ -95,20 +97,30 @@ public class CheckUserHandleExistsIfNoAllowCredentials extends AbstractWebAuthnA
@Nonnull final WebAuthnAuthenticationContext context) {
final Optional<List<PublicKeyCredentialDescriptor>> allowCredentials = requestOptions.getAllowCredentials();
- final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
+ final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
+ final boolean allowedCredentialsRequested =
+ allowCredentials.isPresent() && !allowCredentials.get().isEmpty();
+
+ // Guard to catch a condition which should not occur.
+ if (allowedCredentialsRequested && context.getExistingCredentials().isEmpty()) {
+ log.error("{} Credentials were requested but none exist in the context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return;
+ }
+
if (allowCredentials.isEmpty() || allowCredentials.get() == null || allowCredentials.get().isEmpty()) {
if (userHandle.isEmpty()) {
- log.debug("{} Allow credentials are empty and the userHandle was not returned in the response, the "
- + "userHandle is required in this case", getLogPrefix());
+ log.debug("{} Allow credentials is empty and the userHandle was not returned in the response, the "
+ + "userHandle is required", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
return;
} else {
- log.trace("{} Allow credentials are empty and the userHandle was returned in the response",
+ log.trace("{} Allow credentials is empty and the userHandle was returned in the response",
getLogPrefix());
}
} else {
- log.trace("{} Allow credentials are not empty and the optional userHandle was {}",
+ log.trace("{} Allow credentials is not empty and the (optional) userHandle was {}",
getLogPrefix(), userHandle.isPresent() ? "returned in the response" :
"not returned in the response");
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
index e6fde32..5eae2ad 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
@@ -15,6 +15,7 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
import java.util.Collection;
+import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
@@ -30,6 +31,8 @@ import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -74,6 +77,12 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
/** The credential repository to use.*/
@NonnullAfterInit private WebAuthnCredentialRepository repository;
+ /** The stashed assertion from the context.*/
+ private PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion;
+
+ /** The stashed request options from the context.*/
+ private PublicKeyCredentialRequestOptions requestOptions;
+
/** Constructor. */
public LookupRegisteredCredentialsFromUserHandle() {
@@ -124,21 +133,51 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
checkSetterPreconditions();
noCredentialsEventId = Constraint.isNotEmpty(eventId, "NoCredentialsEventId can not be null or empty");
}
-
+
/** {@inheritDoc} */
- @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Override
+ protected boolean doPreExecute(final ProfileRequestContext profileRequestContext,
+ final WebAuthnAuthenticationContext context) {
+
+ if (!super.doPreExecute(profileRequestContext, context)) {
+ return false;
+ }
- final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion =
- context.getPublicKeyCredentialAssertionResponse();
+ assertion = context.getPublicKeyCredentialAssertionResponse();
if (assertion == null) {
log.error("{} Unable to find Assertion in WebAuthn authentication context", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ requestOptions = context.getPublicKeyCredentialRequestOptions();
+ if (requestOptions == null) {
+ log.error("{} Unable to find credential request options in WebAuthn authentication context",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ final String username = context.getUsername();
+ final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
+
+ // If request options specified allowedCredentials and the context already contains credentials we
+ // do not need to look anything up
+ final Optional<List<PublicKeyCredentialDescriptor>> allowedCredentials = requestOptions.getAllowCredentials();
+ final boolean allowedCredentialsRequested =
+ allowedCredentials.isPresent() && !allowedCredentials.get().isEmpty();
+
+ if (allowedCredentialsRequested && !context.getExistingCredentials().isEmpty()) {
+ log.trace("{} User already has credentials, nothing to lookup", getLogPrefix());
return;
}
- final String username = context.getUsername();
- final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
boolean credentialsFound = false;
if (userHandle.isEmpty()) {
log.debug("{} User could not be found, the authenticator did not supply a userHandle", getLogPrefix());
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index e17202f..88b8c4b 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -161,11 +161,6 @@
<bean id="CheckUserHandleExistsIfNoAllowCredentials" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.CheckUserHandleExistsIfNoAllowCredentials"/>
-
- <bean id="CheckUserHandleAgainstUsername" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.CheckUserHandleAgainstUsername"
- p:credentialRepository="#{getObject('shibboleth.authn.WebAuthn.CredentialRepository') ?: getObject('shibboleth.authn.WebAuthn.DefaultCredentialRepository')}"
- />
<bean id="CheckCredentialPolicy" parent="AbstractWebAuthnAuthenticationAction" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.CheckCredentialPolicy"
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
index e316724..1c330f9 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
@@ -143,54 +143,13 @@
<action-state id="ExtractPublicKeyCredentialAssertion">
<evaluate expression="ExtractPublicKeyCredentialAssertionFromFormRequest"/>
<evaluate expression="'proceed'" />
- <transition on="proceed" to="BranchOnAuthenticationMode" />
+ <transition on="proceed" to="ValidatePublicKeyCredential" />
</action-state>
- <!-- Determine which authentication mode we are using so different actions can apply -->
- <decision-state id="BranchOnAuthenticationMode">
- <if test="IsSecondFactorAuthenticationMode.test(opensamlProfileRequestContext)"
- then="SecondFactorAssertion"
- else="BranchOnPasswordlessOrUsernamelessMode" />
- </decision-state>
-
- <decision-state id="BranchOnPasswordlessOrUsernamelessMode">
- <if test="IsUsernamelessAuthenticationMode.test(opensamlProfileRequestContext)"
- then="UsernamelessAssertion"
- else="BranchOnPasswordlessMode" />
- </decision-state>
-
- <!-- By this point, if we are not operating in any mode, this represents a fundamental problem with the flow -->
- <decision-state id="BranchOnPasswordlessMode">
- <if test="IsPasswordlessAuthenticationMode.test(opensamlProfileRequestContext)"
- then="PasswordlessAssertion"
- else="RuntimeException" />
- </decision-state>
-
- <action-state id="UsernamelessAssertion">
+ <action-state id="ValidatePublicKeyCredential">
+ <!-- catch no userHandle in assertion when empty allowCredentials here, to protected against bad authenticators -->
<evaluate expression="CheckUserHandleExistsIfNoAllowCredentials"/>
- <evaluate expression="LookupRegisteredCredentialsFromUserHandle"/>
- <evaluate expression="'proceed'" />
-
- <transition on="proceed" to="ValidatePublicKeyCredential" />
- </action-state>
-
- <action-state id="SecondFactorAssertion">
- <evaluate expression="CheckUserHandleExistsIfNoAllowCredentials"/>
- <!-- TODO The WebAuthn client should also check the username it is given matches to the userHandle in the assertion response. So this is pre-emptive.-->
- <evaluate expression="CheckUserHandleAgainstUsername"/>
- <evaluate expression="'proceed'" />
- <transition on="proceed" to="ValidatePublicKeyCredential" />
- </action-state>
-
- <action-state id="PasswordlessAssertion">
- <evaluate expression="CheckUserHandleExistsIfNoAllowCredentials"/>
- <!-- TODO The WebAuthn client should also check the username it is given matches to the userHandle in the assertion response. So this is pre-emptive.-->
- <evaluate expression="CheckUserHandleAgainstUsername"/>
- <evaluate expression="'proceed'" />
- <transition on="proceed" to="ValidatePublicKeyCredential" />
- </action-state>
-
- <action-state id="ValidatePublicKeyCredential">
+ <evaluate expression="LookupRegisteredCredentialsFromUserHandle"/>
<evaluate expression="CheckCredentialPolicy"/>
<evaluate expression="ValidateWebAuthnAssertion"/>
<evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
index 7844132..de456be 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
@@ -147,6 +147,7 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
*
* @throws Exception on error
*/
+ @SuppressWarnings("null")
@Test
public void testPasswordlessFlow_UsernameHasNoCredentials() throws Exception {
//Register a credential for use.
@@ -167,8 +168,10 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
setHttpFormRequest("POST", Map.of("j_username", USERNAME));
externalContext.setEventId("proceed");
- result.getSecond().setCurrentState("CollectUsernameView");
- result.getSecond().resume(externalContext);
+ final FlowExecutionImpl flowExec = result.getSecond();
+ assert flowExec != null;
+ flowExec.setCurrentState("CollectUsernameView");
+ flowExec.resume(externalContext);
assertFlowExecutionActive(result.getSecond());
assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
@@ -275,6 +278,82 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
}
+ /**
+ * Checks:
+ *
+ * <ol>
+ * <li>Username entered has 1 credentials</li>
+ * <li>Credential request options contains an allowCredentials with 1 credential</li>
+ * <li>User selects a different credential from the provider, authenticator signs challenge</li>
+ * <li>UserHandle is not returned but user already has existing credentials</li>
+ * <li>fail, on WebAuthn validation step 5, unrequested credential.</li>
+ * </ol>
+ *
+ *
+ * @throws Exception on error
+ */
+ @Test
+ @SuppressWarnings("null")
+ public void testPasswordlessFlow_NoUserHandle_CredentialDoesNotMatchToUserinContext() throws Exception {
+ //Register a credential for use.
+ final CredentialRecord registration =
+ createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
+ credentialRepo.addRegistrationByUsername(USERNAME, registration);
+
+ final CredentialRecord differentRegistration =
+ createCredentialRegistration("different-user", DISPLAY_NAME, "ZGZ3ZWZ3ZQ==", null);
+ credentialRepo.addRegistrationByUsername("different-user", differentRegistration);
+
+ final var prc = buildProfileRequestContext(false, false, USERNAME);
+
+ final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
+ addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("CollectUsernameView", result.getSecond());
+
+ // Do next part of flow, populate the username
+ ExternalContextHolder.setExternalContext(externalContext);
+ // Add a username input here, which is different than the user that registered the credential
+ setHttpFormRequest("POST", Map.of("j_username", USERNAME));
+ externalContext.setEventId("proceed");
+
+ final FlowExecutionImpl flowExec = result.getSecond();
+ assert flowExec != null;
+ flowExec.setCurrentState("CollectUsernameView");
+ flowExec.resume(externalContext);
+
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+ assertPublicKeyCredentialRequestOptions(prc, true, true);
+
+ // Do assertion validation half of flow
+
+ // Get the challenge that was set into the PublicKeyCredentialRequestOptions.
+ // otherwise we will end up signing a different one, which will provide its own test.
+ final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
+ final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertionResponse = createAssertionReponseFrom(
+ differentRegistration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), true);
+
+ final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
+
+ // Re-set external context to holder
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME,
+ assertionResponseJson));
+ externalContext.setEventId("proceed");
+ result.getSecond().setCurrentState("DisplayWebAuthnView");
+ result.getSecond().resume(externalContext);
+
+ // assert end conditions, no existing result or principal. So this should produce a Username principal
+ assertEquals(result.getSecond().getOutcome().getId(), AuthnEventIds.INVALID_CREDENTIALS);
+ assertAuthenticationFailureConditions(prc);
+
+ }
+
/**
* Checks:
*
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowTriggerOnNoUserHandleCredentials.java
similarity index 56%
copy from webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
copy to webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowTriggerOnNoUserHandleCredentials.java
index 7780cfb..9d7742e 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowTriggerOnNoUserHandleCredentials.java
@@ -31,7 +31,6 @@ import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
-import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialAssertionFromFormRequest;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
@@ -39,7 +38,7 @@ import net.shibboleth.shared.collection.Pair;
/**
- * Flow tests for the usernameless flow with active policies.
+ * Flow tests for the passwordless flow.
*/
@ContextConfiguration(
locations = {
@@ -48,10 +47,10 @@ import net.shibboleth.shared.collection.Pair;
"classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml"},
initializers = {
TestWebAuthnEnvironmentApplicationContextInitializer.class,
- TestWebAuthnUsernamelessWithPolicyApplicationContextInitializer.class
+ TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.class
}
)
-public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
+public class TestPasswordlessFlowTriggerOnNoUserHandleCredentials extends AbstractWebAuthnFlowTest{
/** Flow ID. */
@Nonnull public static final String FLOW_ID = "authn/WebAuthn";
@@ -59,17 +58,22 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
/**
* Constructor.
*/
- protected TestUsernameslessFlowWithPolicy() {
- super(FLOW_ID, "proceed");
+ protected TestPasswordlessFlowTriggerOnNoUserHandleCredentials() {
+ super(FLOW_ID);
}
+
/**
* Checks:
*
* <ol>
- * <li>User selects credential</li>
- * <li>Credential matches to a known registered credential</li>
- * <li>Fail, the credential is filtered by the policy</li>
+ * <li>Username entered has 1 credential</li>
+ * <li>Credential request options contains an allowCredentials with 1 credential</li>
+ * <li>User selects the credential from the provider, authenticator signs challenge</li>
+ * <li>UserHandle is NOT returned in the response</li>
+ * <li>idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle is true, but it should not trigger
+ * because the user is already known and has credentials</li>
+ * <li>(Success) The WebAuthn client validates the credential</li>
* </ol>
*
*
@@ -77,75 +81,31 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
*/
@SuppressWarnings("null")
@Test
- public void testUsernamelessFlow_WithPolicy_CredentialRejected_2FAOnly() throws Exception {
- //Register a credential for use that is only suitable for 2FA
+ public void testPasswordlessFlow_NoUserHandleShouldNotTriggerNoCredentials() throws Exception {
+ //Register a credential for use.
final CredentialRecord registration =
- createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
+ createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64,null);
credentialRepo.addRegistrationByUsername(USERNAME, registration);
- final var prc = buildProfileRequestContext(false, false, null);
+ final var prc = buildProfileRequestContext(false, false, USERNAME);
final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
assertFlowExecutionActive(result.getSecond());
- assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
- assertPublicKeyCredentialRequestOptions(prc, false, true);
-
- // Do assertion validation half of flow
+ assertCurrentStateEquals("CollectUsernameView", result.getSecond());
- // Get the challenge that was set into the PublicKeyCredentialRequestOptions.
- // otherwise we will end up signing a different one, which will provide its own test.
- final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
- final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
-
- final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
- assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
-
- final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
-
- // Re-set external context to holder
+ // Do next part of flow, populate the username
ExternalContextHolder.setExternalContext(externalContext);
- setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME,
- assertionResponseJson));
+ setHttpFormRequest("POST", Map.of("j_username", USERNAME));
externalContext.setEventId("proceed");
- result.getSecond().setCurrentState("DisplayWebAuthnView");
- result.getSecond().resume(externalContext);
- // Policy failure will return the user to the webauthn login page
- assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
-
- }
-
- /**
- * Checks:
- *
- * <ol>
- * <li>User selects credential</li>
- * <li>No userHandle in the response.</li>
- * <li>fail, userHandle is required if allowCredentials is empty.</li>
- * </ol>
- *
- *
- * @throws Exception on error
- */
- @SuppressWarnings("null")
- @Test
- public void testUsernamelessFlow_WithPolicy_NoUserHandle() throws Exception {
- //Register a credential for use that is only suitable for 2FA
- final CredentialRecord registration =
- createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
- credentialRepo.addRegistrationByUsername(USERNAME, registration);
+ result.getSecond().setCurrentState("CollectUsernameView");
+ result.getSecond().resume(externalContext);
- final var prc = buildProfileRequestContext(false, false, null);
-
- final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
- addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
-
assertFlowExecutionActive(result.getSecond());
assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
- assertPublicKeyCredentialRequestOptions(prc, false, true);
+ assertPublicKeyCredentialRequestOptions(prc, true, true);
// Do assertion validation half of flow
@@ -155,7 +115,6 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
- //Blank userHandle
assertionResponse = createAssertionReponseFrom(
registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), true);
@@ -169,12 +128,12 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
result.getSecond().setCurrentState("DisplayWebAuthnView");
result.getSecond().resume(externalContext);
+ // assert end conditions, no existing result or principal. So this should produce a Username principal
+ assertEquals(result.getSecond().getOutcome().getId(), "proceed");
+ assertAuthenticationSuccessConditions(prc, true);
- // Should fail, as no user.name or user.id in the context or userHandle in the response
- assertEquals(result.getSecond().getOutcome().getId(), AuthnEventIds.INVALID_CREDENTIALS);
- assertAuthenticationFailureConditions(prc);
-
}
+
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
index 5351b30..c34287b 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
@@ -33,6 +33,7 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
@@ -161,6 +162,55 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
}
+ @SuppressWarnings("null")
+ @Test
+ public void testSecondFactorFlow_UserHandleDoesNotMatchUsername() throws Exception {
+ //Register a credential for use.
+ final CredentialRecord registration =
+ createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
+ credentialRepo.addRegistrationByUsername(USERNAME, registration);
+
+ final CredentialRecord differentUserRegistration =
+ createCredentialRegistration("different-user", DISPLAY_NAME, "d2R3ZXFmZndmd2U=", null);
+ credentialRepo.addRegistrationByUsername("different-user", registration);
+
+ final var prc = buildProfileRequestContext(false, false, USERNAME);
+ buildMfaContext(prc.ensureSubcontext(AuthenticationContext.class), "authn/Password");
+
+ final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
+ addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+ assertPublicKeyCredentialRequestOptions(prc, true, false);
+
+ // Do assertion validation half of flow
+
+ // Get the challenge that was set into the PublicKeyCredentialRequestOptions.
+ // otherwise we will end up signing a different one, which will provide its own test.
+ final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
+ final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ // Blank the userHandle
+ assertionResponse = createAssertionReponseFrom(
+ differentUserRegistration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
+
+ final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
+
+ // Re-set external context to holder
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME,
+ assertionResponseJson));
+ externalContext.setEventId("proceed");
+ result.getSecond().setCurrentState("DisplayWebAuthnView");
+ result.getSecond().resume(externalContext);
+
+ assertEquals(result.getSecond().getOutcome().getId(), AuthnEventIds.INVALID_CREDENTIALS);
+ assertAuthenticationFailureConditions(prc);
+
+ }
+
/*
* The flow will proceed as if first factor passwordless, as no acceptable previous result. So we will end up on the
* CollectUsernameView instead of DisplayWebAuthnView.
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java
index f552bc0..4ddecf9 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java
@@ -135,7 +135,7 @@ public class TestSecondFactorFlowWithFilterPolicy extends AbstractWebAuthnFlowTe
}
/*
- * Tests the 2fa flow when there is not userHandle in the assertion response. Which is allowable if
+ * Tests the 2fa flow when there is no userHandle in the assertion response. Which is allowable if
* the 'allowedCredentials' are supplied to the WebAuthn get request.
*/
@SuppressWarnings("null")
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
index 7780cfb..5d14e86 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
@@ -113,9 +113,11 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
result.getSecond().setCurrentState("DisplayWebAuthnView");
result.getSecond().resume(externalContext);
+ // Just check we had credentials in the context to get the policy from
+ assertEquals(getWebAuthnAuthenticationContext(prc).getExistingCredentials().size(), 1);
// Policy failure will return the user to the webauthn login page
assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
-
+
}
/**
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.java
new file mode 100644
index 0000000..a11f3ed
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * 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
+ *
+ * http://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 net.shibboleth.idp.plugin.authn.webauthn.flow;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.mock.env.MockPropertySource;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link ApplicationContextInitializer} which prepends properties.
+ *
+ * <p>This needs to include the original IdP-test-layer properties and has to be
+ * set to {@link Ordered#LOWEST_PRECEDENCE} or things blow up.</p>
+ */
+ at Order(Ordered.LOWEST_PRECEDENCE)
+public class TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer
+ implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.class);
+
+ /** {@inheritDoc} */
+ @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+ final MockPropertySource mock = new MockPropertySource("passwordless-mock-properties");
+ mock.setProperty("idp.authn.webauthn.usernameless.enabled", "false");
+ mock.setProperty("idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle", "true");
+ applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+ log.info("Prepending usernameless properties '{}'", mock.getSource());
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
index 44d83cc..f2091d3 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
@@ -34,7 +34,9 @@ import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import com.yubico.webauthn.data.UserIdentity;
+import com.yubico.webauthn.data.UserVerificationRequirement;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.plugin.authn.webauthn.authn.WebAuthnAuthenticationEventIds;
@@ -50,6 +52,8 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
private LookupRegisteredCredentialsFromUserHandle lookup;
private UserIdentity userIdentity;
+
+ private PublicKeyCredentialRequestOptions credentialRequestOptions;
@Override
@@ -67,6 +71,16 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
.displayName("Test User")
.id(ByteArray.fromBase64(USER_HANDLE_B64))
.build();
+
+
+ credentialRequestOptions =
+ PublicKeyCredentialRequestOptions.builder()
+ .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+ .rpId(rp.getIdentity().getId())
+ .userVerification(UserVerificationRequirement.REQUIRED)
+ .timeout(Optional.of(60000l))
+ .build();
+ webAuthnContext.setPublicKeyCredentialRequestOptions(credentialRequestOptions);
}
@SuppressWarnings("null")
@@ -123,7 +137,7 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
*/
@SuppressWarnings("null")
@Test
- public void testUserHandleHasCredentials_UsernameInContextIsDifferent() throws Exception {
+ public void testUserHandleHasCredentials_UsernameInContextIsDifferent() throws Exception {
// This is different from the username located from the userhandle
webAuthnContext.setUsername("username-collected");
@@ -231,6 +245,64 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
assertNull(event);
}
+ @SuppressWarnings("null")
+ @Test
+ public void testNoUserHandle_AllowedCredentialsEmpty() throws Exception {
+
+ credentialRequestOptions =
+ PublicKeyCredentialRequestOptions.builder()
+ .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+ .rpId(rp.getIdentity().getId())
+ .userVerification(UserVerificationRequirement.REQUIRED)
+ .timeout(Optional.of(60000l))
+ .build();
+ webAuthnContext.setPublicKeyCredentialRequestOptions(credentialRequestOptions);
+
+ lookup.initialize();
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64), null);
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(attestation.getId())
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialPublicKey())
+ .build();
+
+
+ final CredentialRecord reg = CredentialRecord.builder()
+ .withUserIdentity(userIdentity)
+ .withUsername(USERNAME)
+ .withTransports(new TreeSet<AuthenticatorTransport>())
+ .withRegistrationTime(Instant.now())
+ .withCredential(credential)
+ .withCredentialNickname("Nickname")
+ .withDiscoverable(Optional.of(Boolean.TRUE))
+ .withUserVerified(true)
+ .build();
+
+ credentialRepo.addRegistrationByUsername(USERNAME, reg);
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ // Now generate an assertion (authentication) and check it is valid
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet, null, true);
+
+ webAuthnContext.setPublicKeyCredentialAssertionResponse(assertion);
+
+ final Event event = lookup.execute(src);
+ assertEquals(event.getId(), WebAuthnAuthenticationEventIds.NO_REGISTERED_WEBAUTHN_CREDENTIALS);
+ }
+
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list