[java-idp-integration-tests] branch main updated: Work on WebAuthn Plugin test

Tom Zeller tzeller at dragonacea.biz
Tue Sep 17 23:18:34 UTC 2024


This is an automated email from the git hooks/post-receive script.

tzeller pushed a commit to branch main
in repository java-idp-integration-tests.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-integration-tests.git;a=commit;h=98b8740d4303c9d705d428c11e036b205bd9c80f

The following commit(s) were added to refs/heads/main by this push:
     new 98b8740  Work on WebAuthn Plugin test
98b8740 is described below

commit 98b8740d4303c9d705d428c11e036b205bd9c80f
Author: Tom Zeller <tzeller at dragonacea.biz>
AuthorDate: Tue Sep 17 18:18:25 2024 -0500

    Work on WebAuthn Plugin test
---
 .../shibboleth/idp/integration/tests/BaseTest.java | 124 ++++++
 .../tests/plugins/WebAuthnPluginTest.java          | 428 +++++++++++++++++++++
 .../tests/util/testng/IgnoreTestIntercepter.java   |  25 ++
 .../tests/util/testng/annotation/ChromeOnly.java   |  13 +
 .../truststore.asc                                 | 287 ++++++++++++++
 src/test/webauthn/mfa-authn-config.xml             |  41 ++
 6 files changed, 918 insertions(+)

diff --git a/src/test/java/net/shibboleth/idp/integration/tests/BaseTest.java b/src/test/java/net/shibboleth/idp/integration/tests/BaseTest.java
index 76fbe94..83886a9 100644
--- a/src/test/java/net/shibboleth/idp/integration/tests/BaseTest.java
+++ b/src/test/java/net/shibboleth/idp/integration/tests/BaseTest.java
@@ -60,6 +60,7 @@ import java.util.stream.Stream;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.apache.commons.io.FileUtils;
 import org.apache.hc.client5.http.classic.HttpClient;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
@@ -428,6 +429,14 @@ public abstract class BaseTest {
     @Nonnull
     final public String truststoresResource = "/net/shibboleth/idp/integration/tests/truststores/";
 
+    /** Directory to store test distributions, defaults to "test-distributions/" */
+    @Nonnull
+    public final Path pathToTestDistributions = Paths.get(BaseTest.TEST_DISTRIBUTIONS_DIRECTORY);
+
+    /** Directory to store downloads, defaults to "test-distributions/downloads" */
+    @Nonnull
+    public final Path pathToDownloadsDir = pathToTestDistributions.resolve("downloads");
+
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(BaseTest.class);
 
@@ -3237,4 +3246,119 @@ public abstract class BaseTest {
         }
     }
 
+    /**
+     * Wait for element to be clickable and click it.
+     * 
+     * @param by
+     *            locator
+     */
+    public void click(@Nonnull final By by) {
+        final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
+        wait.until(ExpectedConditions.elementToBeClickable(by));
+        driver.findElement(by).click();
+    }
+
+    /**
+     * Download file at given URL to the downloads directory.
+     * 
+     * See {@link pathToDownloadsDir} for path to downloads directory.
+     * 
+     * Will not download file if it already exists in the downloads directory.
+     * 
+     * @param baseUrl
+     *            URL to download from, for example :
+     *            "https://shibboleth.net/downloads/prerelease/"
+     * @param fileName
+     *            name of file to download, for example :
+     *            "idp-plugin-webauthn-0.0.3.tar.gz"
+     * @throws IOException
+     *             if an error occurs
+     * @throws URISyntaxException
+     *             if the URL is not valid
+     */
+    public void download(@Nonnull final String baseUrl, @Nonnull final String fileName)
+            throws IOException, URISyntaxException {
+
+        final Path pathToDownloadedFile = pathToDownloadsDir.resolve(fileName);
+
+        if (pathToDownloadedFile.toFile().exists()) {
+            return;
+        }
+
+        if (!pathToDownloadsDir.toFile().exists()) {
+            Files.createDirectory(pathToDownloadsDir);
+        }
+
+        final String urlToDownload = baseUrl + fileName;
+
+        log.debug("Downloading '{}' to '{}'", urlToDownload, pathToDownloadedFile);
+
+        FileUtils.copyURLToFile(new URI(urlToDownload).toURL(), pathToDownloadedFile.toFile());
+    }
+
+    /**
+     * Download prelease plugin or distribution and the detached signature.
+     * 
+     * @param fileName
+     *            name of prerelease plugin or distribution
+     * @throws IOException
+     *             if an error occurs
+     * @throws URISyntaxException
+     *             if the URL to download from is invalid
+     */
+    public void downloadPrerelease(@Nonnull final String fileName) throws IOException, URISyntaxException {
+        download("https://shibboleth.net/downloads/prerelease/", fileName);
+        download("https://shibboleth.net/downloads/prerelease/", fileName + ".asc");
+    }
+
+    /**
+     * Install local plugin prerelease.
+     * 
+     * The truststore must exist in the {@link pathToTruststores} directory.
+     * 
+     * @param plugin
+     *            the plugin ID
+     * @param fileName
+     *            the name of the downloaded plugin in the downloads directory
+     * @throws IOException
+     *             if an error occurs
+     */
+    public void installLocalPluginPrerelease(@Nonnull final String plugin, @Nonnull final String fileName)
+            throws IOException {
+
+        log.debug("Install local plugin prerelease '{}'", fileName);
+
+        final Path pathToPlugin = pathToDownloadsDir.resolve(fileName);
+        assert pathToPlugin.toAbsolutePath().toFile().exists() : "Path to plugin not found " + pathToPlugin;
+
+        final Path pathToTruststore = pathToTruststores.resolve(plugin).resolve("truststore.asc");
+        assert pathToTruststore.toAbsolutePath().toFile().exists() : "Path to truststore not found " + pathToTruststore;
+
+        final String[] commands = new String[] { //
+                pluginCLI, //
+                "--noCheck", //
+                "--noRebuild", //
+                "--noPrompt", //
+                "--input ", pathToPlugin.toAbsolutePath().toString(), //
+                "--truststore ", pathToTruststore.toString() };
+
+        log.debug("Install local plugin prerelease with command '{}'", String.join("", commands));
+
+        final Process process = new ProcessBuilder() //
+                .command(commands)
+                .directory(idpHome)
+                .redirectErrorStream(true)
+                .start();
+
+        logProcess(process, "Install local plugin :");
+    }
+
+    /**
+     * Return true if SELENIUM_BROWSER system property is "chrome", ignoring case.
+     * 
+     * @return true if SELENIUM_BROWSER system property is "chrome", ignoring case
+     */
+    public static boolean isChrome() {
+        return System.getProperty("SELENIUM_BROWSER").toLowerCase().equals("chrome");
+    }
 }
diff --git a/src/test/java/net/shibboleth/idp/integration/tests/plugins/WebAuthnPluginTest.java b/src/test/java/net/shibboleth/idp/integration/tests/plugins/WebAuthnPluginTest.java
new file mode 100644
index 0000000..f931927
--- /dev/null
+++ b/src/test/java/net/shibboleth/idp/integration/tests/plugins/WebAuthnPluginTest.java
@@ -0,0 +1,428 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.integration.tests.plugins;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+
+import org.openqa.selenium.Alert;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
+import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator;
+import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.integration.tests.saml2.AbstractSAML2IntegrationTest;
+import net.shibboleth.idp.integration.tests.util.testng.annotation.ChromeOnly;
+
+/**
+ * Some tests for the WebAuthn plugin for the IdP.
+ * 
+ * https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/3395321933/WebAuthnAuthnConfiguration
+ */
+public class WebAuthnPluginTest extends AbstractSAML2IntegrationTest {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(WebAuthnPluginTest.class);
+
+    /**
+     * Download, cache, and install plugin dependencies to speed up tests during
+     * development.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void downloadAndInstallPluginDependencies() throws IOException {
+        try {
+            // "net.shibboleth.oidc.common"
+            download("https://shibboleth.net/downloads/identity-provider/plugins/oidc-common/3.1.1/",
+                    "oidc-common-dist-3.1.1.tar.gz");
+            download("https://shibboleth.net/downloads/identity-provider/plugins/oidc-common/3.1.1/",
+                    "oidc-common-dist-3.1.1.tar.gz.asc");
+            installLocalPlugin("net.shibboleth.oidc.common",
+                    pathToDownloadsDir.resolve("oidc-common-dist-3.1.1.tar.gz").toAbsolutePath().toString());
+
+            // "net.shibboleth.idp.plugin.nashorn"
+            download("https://shibboleth.net/downloads/identity-provider/plugins/duo-oidc/2.1.0/",
+                    "idp-plugin-duo-nimbus-dist-2.1.0.tar.gz");
+            download("https://shibboleth.net/downloads/identity-provider/plugins/duo-oidc/2.1.0/",
+                    "idp-plugin-duo-nimbus-dist-2.1.0.tar.gz.asc");
+            installLocalPlugin("net.shibboleth.idp.plugin.authn.duo.nimbus",
+                    pathToDownloadsDir.resolve("idp-plugin-duo-nimbus-dist-2.1.0.tar.gz").toAbsolutePath().toString());
+
+            // "net.shibboleth.idp.plugin.authn.duo.nimbus"
+            download("https://shibboleth.net/downloads/identity-provider/plugins/scripting/2.0.0/",
+                    "idp-plugin-nashorn-jdk-dist-2.0.0.tar.gz");
+            download("https://shibboleth.net/downloads/identity-provider/plugins/scripting/2.0.0/",
+                    "idp-plugin-nashorn-jdk-dist-2.0.0.tar.gz.asc");
+            installLocalPlugin("net.shibboleth.idp.plugin.nashorn",
+                    pathToDownloadsDir.resolve("idp-plugin-nashorn-jdk-dist-2.0.0.tar.gz").toAbsolutePath().toString());
+        } catch (final URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Install the WebAuthn Plugin and dependencies.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void installWebAuthnPlugin() throws IOException {
+
+        final String[] plugins = new String[] { //
+                "net.shibboleth.oidc.common", //
+                "net.shibboleth.idp.plugin.authn.duo.nimbus", //
+                "net.shibboleth.idp.plugin.nashorn" };
+
+        // TODO Include WebAuthn Plugin after 1.0 is released
+        // "net.shibboleth.idp.plugin.authn.webauthn"
+        // installPlugins(plugins);
+        downloadAndInstallPluginDependencies();
+        installWebAuthnPluginPrelease();
+
+        assertPluginsAreInstalled(plugins);
+    }
+
+    /**
+     * Install WebAuthn Plugin prerelease.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void installWebAuthnPluginPrelease() throws IOException {
+        try {
+
+            final String plugin = "net.shibboleth.idp.plugin.authn.webauthn";
+
+            final String fileName = "idp-plugin-webauthn-0.0.3.tar.gz";
+
+            downloadPrerelease(fileName);
+
+            installLocalPluginPrerelease(plugin, fileName);
+
+            assertPluginsAreInstalled(plugin);
+
+        } catch (final URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Enter credential nickname.
+     * 
+     * @param nickname
+     *            credential nickname
+     */
+    protected void sendCredentialNickname(final @Nonnull String nickname) {
+
+        final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
+
+        final Alert alert = wait.until(ExpectedConditions.alertIsPresent());
+
+        driver.switchTo().alert();
+
+        alert.sendKeys(nickname);
+
+        alert.accept();
+    }
+
+    /**
+     * Log in passwordless using "jdoe" as username.
+     */
+    protected void loginPasswordless() {
+        loginPasswordless("jdoe");
+    }
+
+    /**
+     * Log in passwordless.
+     * 
+     * @param username
+     *            user name
+     */
+    protected void loginPasswordless(final @Nonnull String username) {
+
+        final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
+
+        wait.until(driver -> driver.findElement(By.name("j_username")));
+
+        final WebElement usernameElement = driver.findElement(By.name("j_username"));
+
+        usernameElement.sendKeys(username);
+
+        submitForm();
+    }
+
+    /**
+     * Set up access control policy to allow current user.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpAccessControlPolicy() throws IOException {
+
+        final Path pathToAccessControlXML = Paths.get("conf", "access-control.xml");
+
+        final String oldText = "    </util:map>";
+
+        final String newText = //
+                "    <entry key=\"AccessByCurrentUser\">" + "\n" + //
+                        "        <bean parent=\"shibboleth.PredicateAccessControl\">" + "\n" + //
+                        "            <constructor-arg>" + "\n" + //
+                        "                <bean id=\"AccessByCurrentUserPredicate\"" + "\n" + //
+                        "                    class=\"net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AllowCurrentUserAccessPredicate\">\n"
+                        + //
+                        "                </bean>" + "\n" + //
+                        "            </constructor-arg>" + "\n" + //
+                        "        </bean>" + "\n" + //
+                        "    </entry>" + "\n";
+
+        replaceIdPHomeFile(pathToAccessControlXML, oldText, newText + "\n" + oldText);
+    }
+
+    /**
+     * Do not enable attribute consent.
+     */
+    @BeforeClass
+    @Override
+    public void setUpAttributeConsent() throws IOException {
+        // do not enable attribute consent
+    }
+
+    /**
+     * Set up MFA authn flow.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpMFAFlow() throws IOException {
+
+        final Path pathToWebAuthnProperties = Paths.get("conf", "authn", "authn.properties");
+
+        final String oldFlows = "#idp.authn.flows = Password";
+
+        final String newFlows = "idp.authn.flows = MFA";
+
+        replaceIdPHomeFile(pathToWebAuthnProperties, oldFlows, newFlows);
+    }
+
+    /**
+     * Set up MFA strategy to use WebAuthn as the sole factor but with password
+     * fallback for registration.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpMFAStrategy() throws IOException {
+
+        final Path projectDir = pathToTestDistributions.toAbsolutePath().toFile().getParentFile().toPath();
+
+        assert projectDir.toAbsolutePath().toFile().exists() : "Path to project directory not found";
+
+        final Path pathToNewConfig = projectDir.resolve(Paths.get("src", "test", "webauthn", "mfa-authn-config.xml"));
+
+        assert pathToNewConfig.toAbsolutePath().toFile().exists() : "Path to new mfa-authn-config.xml not found";
+
+        final Path pathToMFAAuthnConfig = pathToIdPHome.resolve(Paths.get("conf", "authn", "mfa-authn-config.xml"));
+
+        Files.copy(pathToNewConfig, pathToMFAAuthnConfig, StandardCopyOption.REPLACE_EXISTING);
+    }
+
+    /**
+     * Set up SAML2 Redirect URLs for testing. -
+     */
+    protected void setUpSAML2URLs() {
+
+        startFlowURLPath = "/sp/SAML2/InitSSO/Redirect";
+
+        loginPageURLPath = "/idp/profile/SAML2/Redirect/SSO";
+
+        forceAuthnRequestURLPath = "/sp/SAML2/InitSSO/ForceAuthn";
+
+        responsePageURLPath = "/sp/SAML2/POST/ACS";
+    }
+
+    /**
+     * Set up virtual authenticator.
+     * 
+     * @return the virtual authenticator
+     */
+    protected VirtualAuthenticator setUpVirtualAuthenticator() {
+
+        final VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions() //
+                .setIsUserVerified(true)
+                .setHasUserVerification(true)
+                .setTransport(VirtualAuthenticatorOptions.Transport.INTERNAL)
+                .setProtocol(VirtualAuthenticatorOptions.Protocol.CTAP2);
+
+        final VirtualAuthenticator authenticator = ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options);
+        log.debug("Set up virutal authenticator '{}'", authenticator);
+        return authenticator;
+    }
+
+    /**
+     * Download, install, and set up WebAuthn Plugin.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     * @throws URISyntaxException
+     *             if a download URL is incorrect
+     */
+    protected void setUpWebAuthnPlugin() throws IOException, URISyntaxException {
+
+        installWebAuthnPlugin();
+
+        enableModule("idp.authn.MFA");
+
+        buildWAR();
+
+        setUpWebAuthnRelyingParty();
+
+        setUpMFAFlow();
+
+        setUpMFAStrategy();
+
+        setUpAccessControlPolicy();
+    }
+
+    /**
+     * Set up WebAuthn relying party :
+     * 
+     * idp.authn.webauthn.relyingPartyId = tests.shibboleth.net
+     * 
+     * idp.authn.webauthn.allowOriginPort = true
+     * 
+     * idp.authn.webauthn.allowOriginSubdomain = true
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpWebAuthnRelyingParty() throws IOException {
+
+        final Path pathToWebAuthnProperties = Paths.get("conf", "authn", "webauthn.properties");
+
+        final String oldRelyingPartyId = "idp.authn.webauthn.relyingPartyId = localhost";
+
+        final String newRelyingPartyId = "idp.authn.webauthn.relyingPartyId = tests.shibboleth.net";
+
+        replaceIdPHomeFile(pathToWebAuthnProperties, oldRelyingPartyId, newRelyingPartyId);
+
+        final String oldAllowOriginPort = "#idp.authn.webauthn.allowOriginPort = false";
+
+        final String newAllowOriginPort = "idp.authn.webauthn.allowOriginPort = true";
+
+        replaceIdPHomeFile(pathToWebAuthnProperties, oldAllowOriginPort, newAllowOriginPort);
+
+        final String oldAllowOriginSubdomain = "#idp.authn.webauthn.allowOriginSubdomain = false";
+
+        final String newAllowOriginSubdomain = "idp.authn.webauthn.allowOriginSubdomain = true";
+
+        replaceIdPHomeFile(pathToWebAuthnProperties, oldAllowOriginSubdomain, newAllowOriginSubdomain);
+    }
+
+    /**
+     * Test SAML 2 redirect SSO using WebAuthn plugin for authentication.
+     * 
+     * A virtual devices is registered using password fallback.
+     * 
+     * @throws Exception
+     *             if an error occurs
+     */
+    @Test(enabled = false)
+    @ChromeOnly
+    public void testSAML2SSO() throws Exception {
+
+        setUpSAML2URLs();
+
+        startBrowser();
+
+        if (!(driver instanceof ChromeDriver)) {
+            log.info("WebAuthn plugin test is only supported on Chrome");
+            return;
+        }
+
+        setUpWebAuthnPlugin();
+
+        startServer();
+
+        setUpVirtualAuthenticator();
+
+        startFlow();
+
+        waitForLoginPage();
+
+        click(By.linkText("Enroll New Devices"));
+
+        waitForPageWithURL(getBaseURL() + "/idp/profile/admin/webauthn-registration");
+
+        loginPasswordless();
+
+        waitForPageWithURL(getBaseURL() + "/idp/profile/admin/webauthn-registration");
+
+        login();
+
+        click(By.id("registerButton"));
+
+        sendCredentialNickname("Test credential nickname");
+
+        click(By.id("finish_button"));
+
+        waitForPageBodyContains("Your registration session has ended");
+
+        // login
+
+        startFlow();
+
+        // already authenticated
+
+        waitForResponsePage();
+
+        validateResponse();
+
+        // force authn
+
+        driver.get(getBaseURL() + forceAuthnRequestURLPath);
+
+        waitForLoginPage();
+
+        loginPasswordless();
+
+        click(By.id("authenticate"));
+
+        waitForResponsePage();
+
+        validator.authnContextClassRef = "http://example.org/ac/classes/mfa";
+
+        validateResponse();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/net/shibboleth/idp/integration/tests/util/testng/IgnoreTestIntercepter.java b/src/test/java/net/shibboleth/idp/integration/tests/util/testng/IgnoreTestIntercepter.java
index 1fac73f..8a49789 100644
--- a/src/test/java/net/shibboleth/idp/integration/tests/util/testng/IgnoreTestIntercepter.java
+++ b/src/test/java/net/shibboleth/idp/integration/tests/util/testng/IgnoreTestIntercepter.java
@@ -13,6 +13,7 @@ import org.testng.IMethodInterceptor;
 import org.testng.ITestContext;
 
 import net.shibboleth.idp.integration.tests.BaseTest;
+import net.shibboleth.idp.integration.tests.util.testng.annotation.ChromeOnly;
 import net.shibboleth.idp.integration.tests.util.testng.annotation.IdPV5OrLater;
 import net.shibboleth.idp.integration.tests.util.testng.annotation.LinuxOnly;
 import net.shibboleth.idp.integration.tests.util.testng.annotation.OPConformance;
@@ -59,6 +60,30 @@ public class IgnoreTestIntercepter implements IMethodInterceptor {
         return filteredTestMethods;
     }
 
+    /**
+     * Return true if test is annotated as {@link ChromeOnly} and browser is Chrome.
+     * 
+     * See {@link BaseTest#isChrome()}.
+     * 
+     * @param method
+     *            the test method
+     * @return return false if test is annotated as ChromeOnly and browser is
+     *         Chrome, true otherwise.
+     */
+    public boolean chromeOnly(final IMethodInstance method) {
+
+        final ChromeOnly chromeOnly = method.getMethod()
+                .getConstructorOrMethod()
+                .getMethod()
+                .getAnnotation(ChromeOnly.class);
+
+        if (chromeOnly != null && BaseTest.isChrome()) {
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Return false if test is annotated as {@link IdPV5OrLater} and IdP major
      * version is less than 5.
diff --git a/src/test/java/net/shibboleth/idp/integration/tests/util/testng/annotation/ChromeOnly.java b/src/test/java/net/shibboleth/idp/integration/tests/util/testng/annotation/ChromeOnly.java
new file mode 100644
index 0000000..1e23376
--- /dev/null
+++ b/src/test/java/net/shibboleth/idp/integration/tests/util/testng/annotation/ChromeOnly.java
@@ -0,0 +1,13 @@
+
+package net.shibboleth.idp.integration.tests.util.testng.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target(ElementType.METHOD)
+public @interface ChromeOnly {
+
+}
diff --git a/src/test/resources/net/shibboleth/idp/integration/tests/truststores/net.shibboleth.idp.plugin.authn.webauthn/truststore.asc b/src/test/resources/net/shibboleth/idp/integration/tests/truststores/net.shibboleth.idp.plugin.authn.webauthn/truststore.asc
new file mode 100644
index 0000000..30ac76c
--- /dev/null
+++ b/src/test/resources/net/shibboleth/idp/integration/tests/truststores/net.shibboleth.idp.plugin.authn.webauthn/truststore.asc
@@ -0,0 +1,287 @@
+
+
Philip David Smart <philip.smart at jisc.ac.uk>	id	B8A3DC52
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.72.2
+
+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
+AAEAEAAAkgoABQAAAAEAAAKqkhQAAwAAAAQAAAKyknwABwAAA+IAAAK6kpEAAgAA
+AAQ3MTAAkpIAAgAAAAQ3MTAAoAAABwAAAAQwMTAwoAIABAAAAAEAAACWoAMABAAA
+AAEAAADqohcAAwAAAAEAAgAAowEABwAAAAEBAAAApAIAAwAAAAEAAAAApAMAAwAA
+AAEAAAAApAUAAwAAAAEAHAAApAYAAwAAAAEAAAAApDIABQAAAAQAAAacpDMAAgAA
+AAYAAAa8pDQAAgAAACIAAAbCAAAAAAAAAAEAAAARAAAACQAAAAUyMDE4OjA3OjE2
+IDEwOjA3OjM4ADIwMTg6MDc6MTYgMTA6MDc6MzgAAAAKdAAAApMAAAhvAAAE+QAA
+KOIAAA/BAAAAAAAAAAEAAAGPAAAAZAXXBg4DvwPBQXBwbGUgaU9TAAABTU0AEwAB
+AAkAAAABAAAACQACAAcAAAIuAAAA+AADAAcAAABoAAADJgAEAAkAAAABAAAAAQAF
+AAkAAAABAAAA6AAGAAkAAAABAAAA8QAHAAkAAAABAAAAAQAIAAoAAAADAAADjgAM
+AAoAAAACAAADpgANAAkAAAABAAAAKAAOAAkAAAABAAAABAAPAAkAAAABAAAAAgAQ
+AAkAAAABAAAAAQARAAIAAAAlAAADtgAUAAkAAAABAAAABQAXAAkAAAABAAAAAAAZ
+AAkAAAABAAAAAAAaAAIAAAAGAAAD3AAfAAkAAAABAAAAAAAAAABicGxpc3QwME8R
+AgBHAkwCUQJVAlICSwJDAjsCLAIaAnMBzgCQAIgAiAB/AEsCUgJWAlYCTgJGAj4C
+MQIgAsoBjwB/AHkAgACBAIAATgJXAloCVgJOAkQCOAIlAg8CMwFwAHcAjACKAJYA
+jwBMAlYCVwJQAkYCOgIrAhIC4wGiAGUAkgCJAI4AjQCSAEoCUgJQAkcCOgIqAhYC
+9AFqAXgAdACRAIUAhQCMAIsASQJKAvYB+gC8AOIAjgGJAbEAcQBrAIMAeQB6AH4A
+gQBFAoEBTQDQALEA4QDiAL0ArgCZAIkAYwBoAHkAdQBuAEECBAF5ABgBtADHANUA
+uQCkAN0AyQCQAIMAfABqAGUAOwL2AHIAIgEEAb0ArACYAIYAuwDQAK8AXgBmAG8A
+eAAxAgkBUADvAIsAqwC/AJ0AbQB5AF8AZABvAHwAdAB1ACYC5gG3AHUAdgCuANwA
+DAHGAGIAYQBqAHEAdwBvAHAAHQIaAv8BtAGjAZ0BhwF4ATEBcwBtAHYAdQB0AGsA
+ZwAYAhkCDQL5AegB2gHFAagBcgGtAF8AdgBzAHIAagBoABUCGQIUAgkC/AHoAc0B
+sAGPARYBZgBmAHkAagBzAGYAEwITAg8CBAL4AeUBygGzAZsBfgHNAGEAcgBpAGsA
+YQAQAg0CCQL/AfQB4gHJAbMBoAGMAXwBUwEDAeAAcAByAAAIAAAAAAAAAgEAAAAA
+AAAAAQAAAAAAAAAAAAAAAAAAAgxicGxpc3QwMNQBAgMEBQYHCFVmbGFnc1V2YWx1
+ZVl0aW1lc2NhbGVVZXBvY2gQARMAAAU6zneKsRI7msoAEAAIERcdJy0vOD0AAAAA
+AAABAQAAAAAAAAAJAAAAAAAAAAAAAAAAAAAAP///xsEAChy8//+2DAAAShH///v9
+AArWDwAAADsAAAEAAAAAZQAAAQA0MkRDQUE3RC0wMTU0LTRBNTktQjY2NS03QTZB
+QjZBRkM4OEQAAHE4MjVzAAA/1d8AD/+1AD/V3wAP/7UAAAAJAAAABQAAAAkAAAAF
+QXBwbGUAaVBob25lIDcgYmFjayBjYW1lcmEgMy45OW1tIGYvMS44AAANAAEAAgAA
+AAJOAAAAAAIABQAAAAMAAAeGAAMAAgAAAAJXAAAAAAQABQAAAAMAAAeeAAUAAQAA
+AAEAAAAAAAYABQAAAAEAAAe2AAwAAgAAAAJLAAAAAA0ABQAAAAEAAAe+ABAAAgAA
+AAJUAAAAABEABQAAAAEAAAfGABcAAgAAAAJUAAAAABgABQAAAAEAAAfOAB8ABQAA
+AAEAAAfWAAAAAAAAADMAAAABAAAAIAAAAAEAAA5QAAAAZAAAAAMAAAABAAAACwAA
+AAEAABWDAAAAZAAAWb0AAADZAAAArQAABJ4AAN2FAAABWAAA3YUAAAFYAAAACAAA
+AAH/4Q1XaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVn
+aW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBt
+ZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2
+LjAuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5
+OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFi
+b3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIg
+eG1sbnM6bXdnLXJzPSJodHRwOi8vd3d3Lm1ldGFkYXRhd29ya2luZ2dyb3VwLmNv
+bS9zY2hlbWFzL3JlZ2lvbnMvIiB4bWxuczpzdEFyZWE9Imh0dHA6Ly9ucy5hZG9i
+ZS5jb20veG1wL3NUeXBlL0FyZWEjIiB4bWxuczphcHBsZS1maT0iaHR0cDovL25z
+LmFwcGxlLmNvbS9mYWNlaW5mby8xLjAvIiB4bWxuczpzdERpbT0iaHR0cDovL25z
+LmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL0RpbWVuc2lvbnMjIiB4bWxuczpwaG90
+b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcDpD
+cmVhdGVEYXRlPSIyMDE4LTA3LTE2VDEwOjA3OjM4LjcxMCIgeG1wOkNyZWF0b3JU
+b29sPSIxMS40IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOC0wNy0xNlQxMDowNzozOCIg
+cGhvdG9zaG9wOkRhdGVDcmVhdGVkPSIyMDE4LTA3LTE2VDEwOjA3OjM4LjcxMCI+
+IDxtd2ctcnM6UmVnaW9ucyByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+IDxtd2ct
+cnM6UmVnaW9uTGlzdD4gPHJkZjpTZXE+IDxyZGY6bGk+IDxyZGY6RGVzY3JpcHRp
+b24gbXdnLXJzOlR5cGU9IkZhY2UiPiA8bXdnLXJzOkFyZWEgc3RBcmVhOnk9IjAu
+NTEyOTk5OTk5OTk5OTk5OSIgc3RBcmVhOnc9IjAuMjM3OTk5OTk5OTk5OTk5OTki
+IHN0QXJlYTp4PSIwLjM3MSIgc3RBcmVhOmg9IjAuMzE3OTk5OTk5OTk5OTk5OTUi
+IHN0QXJlYTp1bml0PSJub3JtYWxpemVkIi8+IDxtd2ctcnM6RXh0ZW5zaW9ucyBh
+cHBsZS1maTpBbmdsZUluZm9ZYXc9IjAiIGFwcGxlLWZpOkFuZ2xlSW5mb1JvbGw9
+IjI3MCIgYXBwbGUtZmk6Q29uZmlkZW5jZUxldmVsPSIxMDAwIiBhcHBsZS1maTpU
+aW1lc3RhbXA9IjEzNzk5OTkyMTQ1MCIgYXBwbGUtZmk6RmFjZUlEPSI5Ii8+IDwv
+cmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpsaT4gPC9yZGY6U2VxPiA8L213Zy1yczpS
+ZWdpb25MaXN0PiA8bXdnLXJzOkFwcGxpZWRUb0RpbWVuc2lvbnMgc3REaW06aD0i
+MzAyNCIgc3REaW06dz0iNDAzMiIgc3REaW06dW5pdD0icGl4ZWwiLz4gPC9td2ct
+cnM6UmVnaW9ucz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94Onht
+cG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0
+IGVuZD0idyI/PgD/7QB4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAD8cAVoAAxsl
+RxwCAAACAAIcAj8ABjEwMDczOBwCPgAIMjAxODA3MTYcAjcACDIwMTgwNzE2HAI8
+AAYxMDA3MzgAOEJJTQQlAAAAAAAQOGix0QQ3mMOAExf+M5Hq4v/iAkBJQ0NfUFJP
+RklMRQABAQAAAjBBREJFAhAAAG1udHJSR0IgWFlaIAfQAAgACwATADMAO2Fjc3BB
+UFBMAAAAAG5vbmUAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtQURCRQAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmNwcnQA
+AAD8AAAAMmRlc2MAAAEwAAAAa3d0cHQAAAGcAAAAFGJrcHQAAAGwAAAAFHJUUkMA
+AAHEAAAADmdUUkMAAAHUAAAADmJUUkMAAAHkAAAADnJYWVoAAAH0AAAAFGdYWVoA
+AAIIAAAAFGJYWVoAAAIcAAAAFHRleHQAAAAAQ29weXJpZ2h0IDIwMDAgQWRvYmUg
+U3lzdGVtcyBJbmNvcnBvcmF0ZWQAAABkZXNjAAAAAAAAABFBZG9iZSBSR0IgKDE5
+OTgpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA81EA
+AQAAAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAY3VydgAAAAAAAAABAjMAAGN1cnYA
+AAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAWFlaIAAAAAAAAJwYAABPpQAABPxY
+WVogAAAAAAAANI0AAKAsAAAPlVhZWiAAAAAAAAAmMQAAEC8AAL6c/8AAEQgA6gCW
+AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQ
+AAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHw
+JDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp
+anN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TF
+xsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB
+AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUh
+MQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4
+OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWW
+l5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp
+6vLz9PX29/j5+v/bAEMAGBgYGBgYKRgYKTopKSk6Tjo6OjpOY05OTk5OY3djY2Nj
+Y2N3d3d3d3d3d4+Pj4+Pj6enp6enu7u7u7u7u7u7u//bAEMBHR8fMCwwUiwsUsSF
+bYXExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE
+xMTExP/dAAQACv/aAAwDAQACEQMRAD8A6KiiisiwpaSkJ4oAZK2xd3uB+fFP3CqL
+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
+=+Re0
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/src/test/webauthn/mfa-authn-config.xml b/src/test/webauthn/mfa-authn-config.xml
new file mode 100644
index 0000000..5ba7ffe
--- /dev/null
+++ b/src/test/webauthn/mfa-authn-config.xml
@@ -0,0 +1,41 @@
+<?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: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/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <util:map id="shibboleth.authn.MFA.TransitionMap">
+         <entry key="">
+            <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkPasswordOrWebAuthn" />
+        </entry>
+    </util:map>
+
+    <!-- Use the webauthn flow unless a registration context exists and the user does not have any FIDO2 credentials registered,
+         then use a password flow.-->
+    <bean id="checkPasswordOrWebAuthn" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript">
+        <constructor-arg>
+            <value>
+            <![CDATA[
+                nextFlow = "authn/WebAuthn"
+                // Check if we can use a WebAuthn flow, or if the user has no credentials available to them use a password flow
+                webauthnRegCtx = input.getSubcontext("net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext");
+                if (webauthnRegCtx != null){
+                    if (!webauthnRegCtx.isWebAuthnAvailable()){
+                        nextFlow = "authn/Password"
+                    }
+                }       
+                nextFlow;   // pass control to WebAuthnFlow
+            ]]>
+            </value>
+        </constructor-arg>
+    </bean>
+
+</beans>

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


More information about the commits mailing list