[java-idp-plugin-webauthn] branch main updated: Complete flow tests
Phil Smart
philip.smart at jisc.ac.uk
Mon Aug 5 15:04:31 UTC 2024
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=508cbc0c26ac48b15fc5bef50fb78d38dd65c37b
The following commit(s) were added to refs/heads/main by this push:
new 508cbc0 Complete flow tests
508cbc0 is described below
commit 508cbc0c26ac48b15fc5bef50fb78d38dd65c37b
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Aug 5 16:04:27 2024 +0100
Complete flow tests
---
.../webauthn/flow/AbstractWebAuthnFlowTest.java | 94 +++++++++++++++++-----
.../authn/webauthn/flow/TestPasswordlessFlow.java | 58 ++++++++++++-
.../authn/webauthn/flow/TestSecondFactorFlow.java | 47 ++++++++++-
.../authn/webauthn/flow/TestUsernameslessFlow.java | 30 +------
4 files changed, 179 insertions(+), 50 deletions(-)
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
index 8ffe3c6..5da1885 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
@@ -15,6 +15,9 @@
package net.shibboleth.idp.plugin.authn.webauthn.flow;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.time.Instant;
@@ -25,7 +28,7 @@ import java.util.Optional;
import java.util.TreeSet;
import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+import javax.security.auth.Subject;
import org.mockito.Mockito;
import org.opensaml.profile.context.ProfileRequestContext;
@@ -65,11 +68,14 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.UserIdentity;
+import com.yubico.webauthn.data.UserVerificationRequirement;
import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.principal.WebAuthnUserIdPrinicpal;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.IdPStorageServiceCredentialRespository;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
@@ -79,6 +85,7 @@ import net.shibboleth.idp.test.flows.AbstractFlowTest;
import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.collection.Pair;
import net.shibboleth.shared.servlet.impl.HttpServletRequestResponseContext;
@@ -223,6 +230,73 @@ public class AbstractWebAuthnFlowTest extends AbstractFlowTest {
}
+ /**
+ * Assert the PublicKeyCredentialCreationOptions has been created, and is correct.
+ *
+ * @param prc the profile request context
+ * @param allowedCredentials should be expect to see a list of allowed credentials?
+ * @param requiresUv do we require UV?
+ */
+ @SuppressWarnings("null")
+ protected void assertPublicKeyCredentialRequestOptions(final ProfileRequestContext prc,
+ final boolean allowedCredentials, final boolean requiresUv) {
+ final var ac = prc.getSubcontext(AuthenticationContext.class);
+ assertNotNull(ac);
+ final var webauthnCtx = ac.getSubcontext(WebAuthnAuthenticationContext.class);
+ assertNotNull(webauthnCtx);
+ final var creationOptions = webauthnCtx.getPublicKeyCredentialRequestOptions();
+ assertNotNull(creationOptions);
+ assertFalse(creationOptions.getAllowCredentials().isEmpty());
+ if (allowedCredentials) {
+ assertFalse(creationOptions.getAllowCredentials().get().isEmpty());
+ } else {
+ assertTrue(creationOptions.getAllowCredentials().get().isEmpty());
+ }
+ if (requiresUv) {
+ assertTrue(creationOptions.getUserVerification().get().equals(UserVerificationRequirement.REQUIRED));
+ } else {
+ assertFalse(creationOptions.getUserVerification().get().equals(UserVerificationRequirement.REQUIRED));
+ }
+ assertNotNull(creationOptions.getChallenge());
+ assertNotNull(creationOptions.getRpId());
+ }
+
+ /**
+ * Assert conditions required to test the WebAuthn flow has produced a valid authentication result. This includes:
+ * <ul>
+ * <li>A username principal.</li>
+ * <li>A WebAuthnUserIdPrincipal.</li>
+ * <li>An authentication results from the authn/WebAuthn flow.</li>
+ * </ul>
+ *
+ * @param prc the profile request context
+ * @param requiresUsernamePrincipal set to true if the WebAuthn authentication result should contain a
+ * username principal. Only false if we assume a previous factor contains a UsernamePrincipal.
+ * @throws EncodingException on error
+ */
+ @SuppressWarnings("null")
+ protected void assertAuthenticationSuccessConditions(final ProfileRequestContext prc,
+ final boolean requiresUsernamePrincipal) throws EncodingException {
+ assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+ final var ac = prc.getSubcontext(AuthenticationContext.class);
+ assertNotNull(ac.getAuthenticationResult());
+ final var webAuthnContext = ac.getSubcontext(WebAuthnAuthenticationContext.class);
+ final var authnResult = ac.getAuthenticationResult();
+ assertEquals(authnResult.getAuthenticationFlowId(), "authn/WebAuthn");
+ final Subject subject = authnResult.getSubject();
+ if (requiresUsernamePrincipal) {
+ assertNotNull(subject.getPrincipals(UsernamePrincipal.class));
+ assertEquals(subject.getPrincipals(UsernamePrincipal.class).size(), 1);
+ assertEquals(subject.getPrincipals(UsernamePrincipal.class).iterator().next().getName(), USERNAME);
+ }
+ assertNotNull(subject.getPrincipals(WebAuthnUserIdPrinicpal.class));
+ assertEquals(subject.getPrincipals(WebAuthnUserIdPrinicpal.class).size(), 1);
+ final byte[] userIdBytes = webAuthnContext.getUserId();
+ final String userId = Base64Support.encode(userIdBytes, false);
+ assertEquals(subject.getPrincipals(WebAuthnUserIdPrinicpal.class).iterator().next().getName(),userId);
+ }
+
+
/**
* Get the {@link WebAuthnAuthenticationContext}.
*
@@ -448,24 +522,6 @@ public class AbstractWebAuthnFlowTest extends AbstractFlowTest {
request.setContentType(contentType);
request.setContent(body.getBytes());
}
-
- /**
- * Test whether a flag is non-null and true.
- *
- * @param flag input flag
- */
- protected void assertTrue(@Nullable final Boolean flag) {
- Assert.assertTrue(flag != null && flag);
- }
-
- /**
- * Test whether a flag is null or false.
- *
- * @param flag input flag
- */
- protected void assertFalse(@Nullable final Boolean flag) {
- Assert.assertTrue(flag == null || !flag);
- }
}
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 f4b6682..d841df0 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
@@ -14,16 +14,26 @@
package net.shibboleth.idp.plugin.authn.webauthn.flow;
-import java.io.IOException;
+import static org.testng.Assert.assertEquals;
+
import java.util.Map;
import javax.annotation.Nonnull;
import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.context.ExternalContextHolder;
import org.springframework.webflow.engine.impl.FlowExecutionImpl;
import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.annotations.Test;
+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.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialAssertionFromFormRequest;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.shared.collection.Pair;
@@ -52,8 +62,14 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
}
/* Should end on username collection when starting the flow.*/
+ @SuppressWarnings("null")
@Test
- public void testPasswordlessFlow_ToCollectUsernameView() throws IOException {
+ public void testPasswordlessFlow() throws Exception {
+ //Register a credential for use.
+ final CredentialRegistration registration =
+ createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64);
+ credentialRepo.addRegistrationByUsername(USERNAME, registration);
+
final var prc = buildProfileRequestContext(false, false, USERNAME);
final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
@@ -61,6 +77,44 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
assertFlowExecutionActive(result.getSecond());
assertCurrentStateEquals("CollectUsernameView", result.getSecond());
+
+ // Do next part of flow, populate the username
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of("j_username", USERNAME));
+ externalContext.setEventId("proceed");
+
+ result.getSecond().setCurrentState("CollectUsernameView");
+ result.getSecond().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 = createAssertionReponseFromCredentialID(
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+
+ 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(), "proceed");
+ assertAuthenticationSuccessConditions(prc, true);
+
}
}
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 360e4ac..382f238 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
@@ -14,20 +14,30 @@
package net.shibboleth.idp.plugin.authn.webauthn.flow;
-import java.io.IOException;
+import static org.testng.Assert.assertEquals;
+
import java.util.Map;
import javax.annotation.Nonnull;
import javax.security.auth.Subject;
import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.context.ExternalContextHolder;
import org.springframework.webflow.engine.impl.FlowExecutionImpl;
import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.annotations.Test;
+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.AuthenticationResult;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext;
+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.CredentialRegistration;
import net.shibboleth.shared.collection.Pair;
@@ -55,8 +65,14 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
super(FLOW_ID);
}
+ @SuppressWarnings("null")
@Test
- public void testSecondFactorFlow_ToWebAuthnView() throws IOException {
+ public void testSecondFactorFlow_ToWebAuthnView() throws Exception {
+ //Register a credential for use.
+ final CredentialRegistration registration =
+ createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64);
+ credentialRepo.addRegistrationByUsername(USERNAME, registration);
+
final var prc = buildProfileRequestContext(false, false, USERNAME);
buildMfaContext(prc.ensureSubcontext(AuthenticationContext.class));
@@ -65,6 +81,33 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
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>
+ assertionResponse = createAssertionReponseFromCredentialID(
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+
+ 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, assume existing username principal from the faked password flow
+ assertEquals(result.getSecond().getOutcome().getId(), "proceed");
+ assertAuthenticationSuccessConditions(prc, false);
+
}
private void buildMfaContext(final AuthenticationContext ac) {
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
index c87e7e9..1c2b136 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
@@ -15,12 +15,10 @@
package net.shibboleth.idp.plugin.authn.webauthn.flow;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
import java.util.Map;
import javax.annotation.Nonnull;
-import javax.security.auth.Subject;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.webflow.context.ExternalContextHolder;
@@ -33,13 +31,9 @@ import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.authn.principal.UsernamePrincipal;
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.principal.WebAuthnUserIdPrinicpal;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
-import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.collection.Pair;
@@ -83,8 +77,9 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
assertFlowExecutionActive(result.getSecond());
assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+ assertPublicKeyCredentialRequestOptions(prc, false, true);
- // Do second half of flow
+ // 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.
@@ -107,27 +102,8 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
// assert end conditions, no existing result or principal. So this should produce a Username principal
assertEquals(result.getSecond().getOutcome().getId(), "proceed");
- assertNotNull(prc.getSubcontext(AuthenticationContext.class));
- final var ac = prc.getSubcontext(AuthenticationContext.class);
- assert ac != null;
- assertNotNull(ac.getAuthenticationResult());
- final var authnResult = ac.getAuthenticationResult();
- assert authnResult != null;
- assertEquals(authnResult.getAuthenticationFlowId(), "authn/WebAuthn");
- final Subject subject = authnResult.getSubject();
- assertNotNull(subject.getPrincipals(UsernamePrincipal.class));
- assertEquals(subject.getPrincipals(UsernamePrincipal.class).size(), 1);
- assertEquals(subject.getPrincipals(UsernamePrincipal.class).iterator().next().getName(), USERNAME);
- assertNotNull(subject.getPrincipals(WebAuthnUserIdPrinicpal.class));
- assertEquals(subject.getPrincipals(WebAuthnUserIdPrinicpal.class).size(), 1);
- final byte[] userIdBytes = authnContext.getUserId();
- final String userId = Base64Support.encode(userIdBytes, false);
- assertEquals(subject.getPrincipals(WebAuthnUserIdPrinicpal.class).iterator().next().getName(),userId);
+ assertAuthenticationSuccessConditions(prc, true);
}
-
-
-
-
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list