[java-idp-plugin-webauthn] branch main updated: Add integration flow tests

Phil Smart philip.smart at jisc.ac.uk
Fri Aug 2 16:51:08 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=30f8ff665f36fba902d999e4fe9e69854f4a33ad

The following commit(s) were added to refs/heads/main by this push:
     new 30f8ff6  Add integration flow tests
30f8ff6 is described below

commit 30f8ff665f36fba902d999e4fe9e69854f4a33ad
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Aug 2 17:51:05 2024 +0100

    Add integration flow tests
---
 webauthn-impl/pom.xml                              | 147 ++++++-
 .../webauthn/flow/AbstractWebAuthnFlowTest.java    | 471 +++++++++++++++++++++
 .../authn/webauthn/flow/TestPasswordlessFlow.java  |  66 +++
 .../authn/webauthn/flow/TestSecondFactorFlow.java  |  79 ++++
 .../authn/webauthn/flow/TestUsernameslessFlow.java | 133 ++++++
 ...stWebAuthn2FaApplicationContextInitializer.java |  50 +++
 ...hnEnvironmentApplicationContextInitializer.java |  56 +++
 ...nPasswordlessApplicationContextInitializer.java |  49 +++
 ...nUsernamelessApplicationContextInitializer.java |  49 +++
 .../authn/webauthn/impl/AbstractWebAuthnTest.java  |   2 +-
 .../src/test/resources/logback-webauthn-test.xml   |  25 ++
 .../idp/plugin/authn/webauthn/test-beans.xml       |  20 +
 12 files changed, 1133 insertions(+), 14 deletions(-)

diff --git a/webauthn-impl/pom.xml b/webauthn-impl/pom.xml
index e361d1d..0988b4f 100644
--- a/webauthn-impl/pom.xml
+++ b/webauthn-impl/pom.xml
@@ -123,11 +123,76 @@
             <artifactId>shib-support</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-networking-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-security-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-velocity-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-storage-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-profile-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-profile-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-context</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-resolver-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>com.google.code.findbugs</groupId>
             <artifactId>jsr305</artifactId>
@@ -156,10 +221,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
-            <groupId>org.springframework</groupId>
+            <groupId>${spring.groupId}</groupId>
             <artifactId>spring-beans</artifactId>
             <scope>provided</scope>
         </dependency>
+       
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
@@ -186,6 +252,72 @@
             <artifactId>okhttp-tls</artifactId>
             <scope>test</scope>
         </dependency>
+        
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-conf-impl</artifactId>
+            <version>${idp.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-conf-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-schema</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-spring</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring-webflow.groupId}</groupId>
+            <artifactId>spring-binding</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-expression</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.janino</groupId>
+            <artifactId>janino</artifactId>
+            <scope>test</scope>
+        </dependency>
+        
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-testing</artifactId>
@@ -215,18 +347,7 @@
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-core-impl</artifactId>
             <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>${shib-shared.groupId}</groupId>
-            <artifactId>shib-testing</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>${spring.groupId}</groupId>
-            <artifactId>spring-test</artifactId>
-            <scope>test</scope>
-        </dependency>        
+        </dependency>    
     </dependencies>
 
     <build>
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
new file mode 100644
index 0000000..8ffe3c6
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
@@ -0,0 +1,471 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeSet;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.webflow.context.ExternalContext;
+import org.springframework.webflow.context.ExternalContextHolder;
+import org.springframework.webflow.core.FlowException;
+import org.springframework.webflow.core.collection.LocalAttributeMap;
+import org.springframework.webflow.core.collection.MutableAttributeMap;
+import org.springframework.webflow.definition.FlowDefinition;
+import org.springframework.webflow.engine.impl.FlowExecutionImpl;
+import org.springframework.webflow.execution.FlowExecution;
+import org.springframework.webflow.execution.repository.FlowExecutionLock;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.springframework.webflow.executor.FlowExecutorImpl;
+import org.springframework.webflow.test.MockExternalContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+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.UserIdentity;
+
+import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+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;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.context.SessionContext;
+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.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.servlet.impl.HttpServletRequestResponseContext;
+
+/**
+ * Abstract unit test class for WebAuthn flows.
+ */
+public class AbstractWebAuthnFlowTest extends AbstractFlowTest {
+    
+    protected final static String ORIGIN = "https://idp.example.com";
+    
+    protected final static String RPID = "idp.example.com";
+    
+    protected final static String CHALLENGE_B64 = "dGhpc2lzBaNoYWxsZW5nZQ==";    
+    
+    protected final static String USER_HANDLE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";   
+    
+    protected final static String USERNAME = "test-user";
+    
+    protected final static String DISPLAY_NAME = "test user";
+
+    protected String flowId;
+    
+    protected String endStateId;
+    
+    /** The CBOR friendly json mapper.*/
+    protected ObjectMapper jsonMapper;
+    
+    /** A mock authenticator to use for creating Authenticator Attestations and Assertions etc.*/
+    protected MockAuthenticator mockAuthenticator;
+    
+    /** The relying party.*/
+    protected RelyingParty rp;
+    
+    @Autowired
+    @Qualifier("shibboleth.authn.webauthn.DefaultCredentialRepository")
+    protected IdPStorageServiceCredentialRespository credentialRepo;  
+    
+    protected AbstractWebAuthnFlowTest(final String id) {
+        this(id, END_STATE_ID);
+    }
+
+    protected AbstractWebAuthnFlowTest(final String id, final String endId) {
+        flowId = id;
+        endStateId = endId == null ? END_STATE_ID : endId;
+    }
+
+    /**
+     * Initialize mock request.
+     */
+    @Override
+    @BeforeMethod public void initializeMocks() {
+        //overrideEndStateOutput(flowId, endStateId);
+
+        request = new MockHttpServletRequest();
+        request.setMethod("GET");
+        response = new MockHttpServletResponse();
+        externalContext = new MockExternalContext();
+        externalContext.setNativeRequest(request);
+        externalContext.setNativeResponse(response);
+        
+        jsonMapper = JsonMapper.builder()
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
+                .serializationInclusion(Include.NON_ABSENT)
+                .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+                .addModule(new Jdk8Module())
+                .addModule(new JavaTimeModule())
+                .build();
+        
+        try {
+            // Create a new one for each test, else it holds state about created keys
+            mockAuthenticator = new MockAuthenticator(RPID);
+        } catch (final Exception e) {
+            fail("Can not initialise mock authenticator");
+        }      
+    }
+    
+    /**
+     * Create a client data JSON object as a Java Map.
+     * 
+     * @param type the operation type
+     * @param origin the origin
+     * @param challenge the challenge
+     * @return the clientData map
+     */
+    protected Map<String, String> createClientData(@Nonnull @NotEmpty final String type, 
+            @Nonnull @NotEmpty final String origin,
+            @Nonnull @NotEmpty final String challenge){
+        final HashMap<String, String> obj = new HashMap<>();
+        obj.put("challenge",challenge);
+        obj.put("origin", origin);
+        obj.put("type", type);
+        return obj;        
+    }
+    
+    /**
+     * Create a credential registration with a new attestation response from the mock authenticator.
+     * 
+     * @param username the username
+     * @param display name the display name
+     * @return the credential registration 
+     * 
+     * @throws Exception on error
+     */
+    protected CredentialRegistration createCredentialRegistration(final String username, final String displayName,
+            final String userHandleB64) throws Exception {
+
+        final var user = UserIdentity.builder()
+                .name(username)
+                .displayName(displayName)
+                .id(ByteArray.fromBase64(userHandleB64))
+                .build();
+       
+        
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation = 
+                createAttestationReponse();
+        
+        final var barray = ByteArray.fromBase64(userHandleB64);
+        assert barray.getBase64().equals(userHandleB64);
+        assert Arrays.equals(barray.getBytes(), Base64Support.decode(userHandleB64));
+
+        
+        final RegisteredCredential credential = RegisteredCredential.builder()
+                .credentialId(attestation.getId())
+                .userHandle(ByteArray.fromBase64(userHandleB64))
+                .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+                        .getAttestedCredentialData().get().getCredentialPublicKey())
+                .build();
+         
+         final var reg = CredentialRegistration.builder()
+                 .withUserIdentity(user)
+                 .withTransports(new TreeSet<AuthenticatorTransport>())
+                 .withRegistrationTime(Instant.now())
+                 .withCredential(credential)
+                 .withAttestationMetadata(CollectionSupport.emptySet())
+                 .withCredentialNickname("nickname")
+                 .withDiscoverable(Optional.of(Boolean.TRUE))
+                 .withUserVerified(true)
+                 .build();
+         
+         return reg;
+    }
+    
+
+    /**
+     * Get the {@link WebAuthnAuthenticationContext}.
+     * 
+     * @return the {@link WebAuthnAuthenticationContext}
+     */
+    protected WebAuthnAuthenticationContext getWebAuthnAuthenticationContext(final ProfileRequestContext prc) {
+        return prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(WebAuthnAuthenticationContext.class);
+    }
+    
+    /**
+     * Create a credential registration attestation response from the mock authenticator.
+     * 
+     * @return the credential registration 
+     * 
+     * @throws Exception on error
+     */
+    protected PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+                createAttestationReponse() throws Exception {
+
+        
+        final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+        
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation = 
+                mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate, 
+                        Base64Support.decode(USER_HANDLE_B64));
+        
+       return attestation;
+    }
+    
+    /**
+     * Create a credential authentication assertion response from the mock authenticator.
+     * 
+     * @return the credential registration 
+     * 
+     * @throws Exception on error
+     */
+    protected PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+                createAssertionReponse() throws Exception {
+        
+        //need to create on first
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation = 
+                    createAttestationReponse();
+        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);
+        
+       return assertion;
+    }
+    
+    /**
+     * Create a credential authentication assertion response from the mock authenticator.
+     * 
+     * @return the credential registration 
+     * 
+     * @throws Exception on error
+     */
+    protected PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+                createAssertionReponseFromCredentialID(
+                        final byte[] credentialId, final byte[] challenge) throws Exception {
+
+        final String challengeB64 = Base64Support.encodeURLSafe(challenge);
+        final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, challengeB64); 
+        
+        // Now generate an assertion (authentication) and check it is valid
+        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> 
+            assertion = mockAuthenticator.createAuthenticatorAssertionResponse(credentialId, 
+                    clientDataGet);
+        
+       return assertion;
+    }
+    
+    /**
+     * Build a {@link ProfileRequestContext} by configuring a suitable context tree e.g. a 
+     * {@link AuthenticationContext}.
+     * 
+     * @param forceAuthn force authentication
+     * @param addC14Context add the c14 context, yes or no.
+     * @param principalName the principal name to add to the c14n context
+     * @return a profile request context.
+     */
+    @Nonnull protected ProfileRequestContext buildProfileRequestContext(final boolean forceAuthn,
+            final boolean addC14Context, final String principalName) {
+        
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final AuthenticationContext ac = new AuthenticationContext();
+        final AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
+        afd.setId("authn/WebAuthn");
+        ac.setAttemptedFlow(afd);
+        ac.addSubcontext(new RelyingPartyUIContext());
+        ac.setForceAuthn(forceAuthn);
+        
+        prc.addSubcontext(ac);
+        if (addC14Context) {
+        final SubjectCanonicalizationContext scc = new SubjectCanonicalizationContext();
+            scc.setPrincipalName(principalName);
+            prc.addSubcontext(scc);
+        }
+        final SessionContext sc = new SessionContext();
+        final IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(session.getPrincipalName()).thenReturn(principalName);
+        sc.setIdPSession(session);
+        prc.addSubcontext(sc);
+        
+        prc.setBrowserProfile(true);
+        return prc;
+    }
+    
+    /**
+     * Copy of {@link FlowExecutorImpl#launchExecution(String, MutableAttributeMap, ExternalContext)} to allow 
+     * conversation scope variables to be set before execution.
+     * 
+     * <p>Adds, calledAsSubflow to the input map.</p>
+     * 
+     * @param flowId the flowID
+     * @param input input to pass to the new execution on startup (optional)
+     * @param context access to the calling environment (required)
+     * @param conversationScope parameters to pass to the conversation scope of the flow execution
+     * @return the result
+     * @throws FlowException on error
+     */
+    protected Pair<FlowExecutionResult, FlowExecutionImpl> launchExecution(final String flowId, MutableAttributeMap<Object> input, 
+            final ExternalContext context, final MutableAttributeMap<?> conversationScope) throws FlowException {
+        try {
+            if (flowExecutor instanceof final FlowExecutorImpl flowExec) {
+                ExternalContextHolder.setExternalContext(context);
+                final FlowDefinition flowDefinition = flowExec.getDefinitionLocator().getFlowDefinition(flowId);
+                final FlowExecution flowExecution = flowExec.getExecutionFactory().createFlowExecution(flowDefinition);
+                flowExecution.getConversationScope().putAll(conversationScope);
+                if (input == null) {
+                    input = new LocalAttributeMap<>();
+                    input.put("calledAsSubflow", true);
+                } else {
+                    input.put("calledAsSubflow", true);
+                }
+                flowExecution.start(input, externalContext);                 
+             
+                if (!flowExecution.hasEnded()) {                    
+                    final FlowExecutionLock lock = flowExec.getExecutionRepository().getLock(flowExecution.getKey());
+                    lock.lock();
+                    try {
+                        flowExec.getExecutionRepository().putFlowExecution(flowExecution);
+                    } finally {
+                        lock.unlock();
+                    }
+                    return new Pair<>(createPausedResult(flowExecution), (FlowExecutionImpl)flowExecution);
+                } else {
+                    return new Pair<>(createEndResult(flowExecution), (FlowExecutionImpl)flowExecution);
+                }
+            } else {
+                fail("Flow executor was the wrong type");
+                // Will not get here.
+                return null;
+            }
+        } finally {
+            ExternalContextHolder.setExternalContext(null);
+        }
+    }
+
+    
+    /**
+     * Assert that the current state of the flow execution equals the provided state id.
+     * @param expectedCurrentStateId the expected current state
+     */
+    protected void assertCurrentStateEquals(final String expectedCurrentStateId, final FlowExecution flowExecution) {
+        assertEquals(flowExecution.getActiveSession().getState().getId(), expectedCurrentStateId, "The current state '" 
+                    + flowExecution.getActiveSession().getState().getId()
+                + "' does not equal the expected state '" + expectedCurrentStateId + "'");
+    }
+    
+    /**
+     * Assert that the entire flow execution is active; that is, it has not ended and has been started.
+     */
+    protected void assertFlowExecutionActive(final FlowExecution flowExecution) {
+        Assert.assertTrue( flowExecution.isActive(), "The flow execution is not active but it should be");
+    }
+    
+    protected LocalAttributeMap<Object> addToConversationScopeMap(final Map<String, Object> things){
+        final LocalAttributeMap<Object> conversationScope = new LocalAttributeMap<>();
+        things.entrySet().forEach(entry -> conversationScope.put(entry.getKey(), entry.getValue()));
+        return conversationScope;
+        
+    }
+    
+    private FlowExecutionResult createEndResult(final FlowExecution flowExecution) {
+        return FlowExecutionResult.createEndedResult(flowExecution.getDefinition().getId(), flowExecution.getOutcome());
+    }
+
+    private FlowExecutionResult createPausedResult(final FlowExecution flowExecution) {
+        return FlowExecutionResult.createPausedResult(flowExecution.getDefinition().getId(), flowExecution.getKey()
+                .toString());
+    }
+    
+    /**
+     * Load the mock request/response objects.
+     */
+    @Override
+    @BeforeMethod public void initializeThreadLocals() {
+        HttpServletRequestResponseContext.loadCurrent(request, response);
+    }
+        
+    protected void setJsonRequest(final String method, final String body) {
+        setRequest(method, body, "application/json");
+    }
+    
+    protected void setHttpFormRequest(final String method, final Map<String, String> parameters) {
+        setHttpFormRequest(request, method, parameters);
+    }
+
+    protected static void setHttpFormRequest(final MockHttpServletRequest request, final String method, final Map<String, String> parameters) {
+        setRequest(request, method, "", "application/x-www-form-urlencoded");
+        request.setParameters(parameters);
+    }
+
+    protected void setRequest(final String method, final String body, final String contentType) {
+        setRequest(request, method, body, contentType);
+    }
+
+    protected static void setRequest(final MockHttpServletRequest request, final String method, final String body, final String contentType) {
+        request.setMethod(method);
+        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
new file mode 100644
index 0000000..f4b6682
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.engine.impl.FlowExecutionImpl;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.Test;
+
+import net.shibboleth.shared.collection.Pair;
+
+
+/**
+ * Flow tests for the passwordless flow.
+ */
+ at ContextConfiguration(
+        locations = {
+                "classpath*:/META-INF/net.shibboleth.idp/postconfig.xml",
+                "classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml", },
+        initializers = {
+                TestWebAuthnEnvironmentApplicationContextInitializer.class,
+                TestWebAuthnPasswordlessApplicationContextInitializer.class
+                }
+        )
+public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "authn/WebAuthn";
+
+    /**
+     * Constructor.
+     */
+    protected TestPasswordlessFlow() {
+        super(FLOW_ID);
+    }
+    
+    /* Should end on username collection when starting the flow.*/
+    @Test
+    public void testPasswordlessFlow_ToCollectUsernameView() throws IOException {
+        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());
+    }
+
+}
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
new file mode 100644
index 0000000..360e4ac
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.security.auth.Subject;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.engine.impl.FlowExecutionImpl;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext;
+import net.shibboleth.shared.collection.Pair;
+
+
+/**
+ * Flow tests for the 2fa flow.
+ */
+ at ContextConfiguration(
+        locations = {
+                "classpath*:/META-INF/net.shibboleth.idp/postconfig.xml",
+                "classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml", },
+        initializers = {
+                TestWebAuthnEnvironmentApplicationContextInitializer.class,
+                TestWebAuthn2FaApplicationContextInitializer.class
+                }
+        )
+public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "authn/WebAuthn";
+
+    /**
+     * Constructor.
+     */
+    protected TestSecondFactorFlow() {
+        super(FLOW_ID);
+    }
+    
+    @Test
+    public void testSecondFactorFlow_ToWebAuthnView() throws IOException {
+        final var prc = buildProfileRequestContext(false, false, USERNAME);
+        buildMfaContext(prc.ensureSubcontext(AuthenticationContext.class));
+
+        final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext, 
+                addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+        assertFlowExecutionActive(result.getSecond());
+        assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+    }
+    
+    private void buildMfaContext(final AuthenticationContext ac) {
+        
+        final MultiFactorAuthenticationContext mfaCtx = new MultiFactorAuthenticationContext();
+        // Create a fake password flow result
+        final AuthenticationResult result = new AuthenticationResult("password", new Subject());        
+        mfaCtx.getActiveResults().put("authn/Password", result);
+        ac.addSubcontext(mfaCtx);
+    }
+
+}
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
new file mode 100644
index 0000000..c87e7e9
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
@@ -0,0 +1,133 @@
+/*
+ * 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 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;
+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.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;
+
+
+/**
+ * Flow tests for the usernameless flow.
+ */
+ at ContextConfiguration(
+        locations = {
+                "classpath*:/META-INF/net.shibboleth.idp/postconfig.xml",
+                "classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml", },
+        initializers = {
+                TestWebAuthnEnvironmentApplicationContextInitializer.class,
+                TestWebAuthnUsernamelessApplicationContextInitializer.class
+                }
+        )
+public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "authn/WebAuthn";
+
+    /**
+     * Constructor.
+     */
+    protected TestUsernameslessFlow() {
+        super(FLOW_ID, "proceed");
+    }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testUsernamelessFlow() 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, null);
+
+        final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext, 
+                addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+        assertFlowExecutionActive(result.getSecond());
+        assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+        
+        // Do second 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");
+        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);
+
+    }
+
+    
+    
+    
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthn2FaApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthn2FaApplicationContextInitializer.java
new file mode 100644
index 0000000..b4c0f1a
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthn2FaApplicationContextInitializer.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 TestWebAuthn2FaApplicationContextInitializer
+        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthn2FaApplicationContextInitializer.class);
+
+    /** {@inheritDoc} */
+    @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+        final MockPropertySource mock = new MockPropertySource("2fa-mock-properties");
+        mock.setProperty("idp.authn.webauthn.2fa.enabled", "true");
+        mock.setProperty("idp.authn.webauthn.2fa.allowedPreviousFactors", "authn/Password");
+        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/flow/TestWebAuthnEnvironmentApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnEnvironmentApplicationContextInitializer.java
new file mode 100644
index 0000000..c499db2
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnEnvironmentApplicationContextInitializer.java
@@ -0,0 +1,56 @@
+/*
+ * 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 TestWebAuthnEnvironmentApplicationContextInitializer
+        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthnEnvironmentApplicationContextInitializer.class);
+
+    /** {@inheritDoc} */
+    @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+        final MockPropertySource mock = new MockPropertySource();
+        mock.setProperty("idp.home", "classpath:/net/shibboleth/idp/module");
+        mock.setProperty("idp.webflows", "classpath*:/flows");
+        mock.setProperty("idp.authn.webauthn.relyingPartyId", "idp.example.com");
+        mock.setProperty("idp.csrf.enabled", "false");
+        mock.setProperty("idp.authn.webauthn.relyingPartyName", "Shibboleth");
+        mock.setProperty("idp.service.logging.resource", "/logback-webauthn-test.xml");
+        mock.setProperty("idp.additionalProperties",
+                "/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties");
+        applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+        log.info("Prepending properties '{}'", mock.getSource());
+    }
+    
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessApplicationContextInitializer.java
new file mode 100644
index 0000000..02188c5
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessApplicationContextInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * 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 TestWebAuthnPasswordlessApplicationContextInitializer
+        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthnPasswordlessApplicationContextInitializer.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");
+        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/flow/TestWebAuthnUsernamelessApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnUsernamelessApplicationContextInitializer.java
new file mode 100644
index 0000000..b02da62
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnUsernamelessApplicationContextInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * 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 TestWebAuthnUsernamelessApplicationContextInitializer
+        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthnUsernamelessApplicationContextInitializer.class);
+
+    /** {@inheritDoc} */
+    @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+        final MockPropertySource mock = new MockPropertySource("usernameless-mock-properties");
+        mock.setProperty("idp.authn.webauthn.usernameless.enabled", "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/AbstractWebAuthnTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
index 0ac5c24..51d00af 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -104,7 +104,7 @@ public abstract class AbstractWebAuthnTest {
     /** The authentication context.*/
     protected AuthenticationContext ac;
     
-    /** A mock authenticator to use for creating Authenticator Attestations etc.*/
+    /** A mock authenticator to use for creating Authenticator Attestations and Assertions.*/
     protected MockAuthenticator mockAuthenticator;
     
     /** The CBOR friendly json mapper.*/
diff --git a/webauthn-impl/src/test/resources/logback-webauthn-test.xml b/webauthn-impl/src/test/resources/logback-webauthn-test.xml
new file mode 100644
index 0000000..d71e445
--- /dev/null
+++ b/webauthn-impl/src/test/resources/logback-webauthn-test.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%level [%logger:%line] - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <root>
+        <level value="WARN" />
+        <appender-ref ref="STDOUT" />
+    </root>
+    
+    <logger name="net.shibboleth.idp.plugin.authn" level="TRACE" additivity="false">
+        <appender-ref ref="STDOUT" />
+    </logger>
+     
+    <logger name="org.springframework.webflow" level="INFO" additivity="false">
+        <appender-ref ref="STDOUT" />
+    </logger>
+    
+</configuration>
\ No newline at end of file
diff --git a/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml b/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml
new file mode 100644
index 0000000..ca9b264
--- /dev/null
+++ b/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+      
+   
+
+</beans>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list