[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