[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