[java-idp-plugin-webauthn] branch main updated: Add admin flow and authentication flow project structure
Phil Smart
philip.smart at jisc.ac.uk
Wed Nov 1 17:48:30 UTC 2023
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=51ca8dda068d8557fc238fb4587f5b06a27039be
The following commit(s) were added to refs/heads/main by this push:
new 51ca8dd Add admin flow and authentication flow project structure
51ca8dd is described below
commit 51ca8dda068d8557fc238fb4587f5b06a27039be
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Nov 1 17:48:23 2023 +0000
Add admin flow and authentication flow project structure
- Authentication flow chooser requires MFA configuration to operate
---
pom.xml | 53 ++++--
webauthn-api/pom.xml | 25 +++
.../AbstractWebAuthnAuthenticationAction.java | 148 +++++++++++++++
.../plugin/authn/webauthn/PublicKeyCredential.java | 78 ++++++++
.../idp/plugin/authn/webauthn/Response.java | 64 +++++++
.../webauthn/WebauthnAuthenticationClient.java | 39 ++++
.../WebauthnAuthenticationClientException.java | 51 +++++
.../WebauthnAuthenticationClientFactory.java | 20 ++
...bAuthnCredentialRegistrationFlowDescriptor.java | 39 ++++
.../context/WebAuthnAuthenticationContext.java | 200 ++++++++++++++++++++
.../context/WebAuthnPublicKeyCredentialRecord.java | 49 +++++
webauthn-impl/pom.xml | 62 +++++-
.../impl/YubicoWebauthnAuthenticationClient.java | 100 ++++++++++
.../client/impl/YubicoWebauthnClientFactory.java | 208 +++++++++++++++++++++
.../CreatePublicKeyCredentialRequestOptions.java | 72 +++++++
.../ExtractPublicKeyAssertionFromFormRequest.java | 138 ++++++++++++++
.../ExtractPublicKeyCredentialFromFormRequest.java | 139 ++++++++++++++
.../webauthn/impl/GenerateServerChallenge.java | 82 ++++++++
.../PopulateWebauthnAuthenticationContext.java | 127 +++++++++++++
...ameFromAuthenticationContextLookupStrategy.java | 55 ++++++
.../webauthn/impl/ValidatePublicKeyCredential.java | 73 ++++++++
.../webauthn/impl/ValidateWebAuthnAssertion.java | 81 ++++++++
.../storage/impl/DummyCredentialRepository.java | 66 +++++++
.../storage/impl/StorePublicKeyCredential.java | 161 ++++++++++++++++
...bauthnPublicKeyCredentialStorageSerializer.java | 59 ++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 42 +++++
.../webauthn-registration-beans.xml | 43 +++++
.../webauthn-registration-flow.xml | 78 ++++++++
.../authn/WebAuthn/webauthn-abstract-beans.xml | 54 ++++++
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 30 ++-
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 38 +++-
.../authn/WebAuthnFlowChooser/webauthn-flow.xml | 18 ++
.../idp/plugin/authn/webauthn/js/webauthn-shib.js | 163 ++++++++++++++++
.../plugin/authn/webauthn/js/webauthn-yubico.js | 171 +++++++++++++++++
.../plugin/authn/webauthn/views/webauthn-authn.vm | 72 +++++++
.../authn/webauthn/views/webauthn-register.vm | 73 ++++++++
.../authn/webauthn/views/webauthn-registered.vm | 57 ++++++
.../authn/webauthn/views/webauthn-selector.vm | 66 +++++++
38 files changed, 3059 insertions(+), 35 deletions(-)
diff --git a/pom.xml b/pom.xml
index 9297a47..3c6f578 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,17 +31,21 @@
<shib-metadata.version>5.0.0</shib-metadata.version>
<yubico.groupId>com.yubico</yubico.groupId>
<yubico-webauthn.version>2.5.0</yubico-webauthn.version>
- <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+ <jackson-dataformat-cbor.verison>2.15.3</jackson-dataformat-cbor.verison>
+ <com-upokecenter.version>4.5.2</com-upokecenter.version>
+ <checkstyle.configLocation>
+ ${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
</properties>
<distributionManagement>
<site>
<id>site</id>
- <url>scp:${shibboleth.site.deploy.url}java-idp-plugin-oidc-rp/${project.version}/</url>
+ <url>
+ scp:${shibboleth.site.deploy.url}java-idp-plugin-oidc-rp/${project.version}/</url>
</site>
</distributionManagement>
-
- <modules>
+
+ <modules>
<module>webauthn-impl</module>
<module>webauthn-api</module>
<!-- All DIST modules must come after the -api and -impl modules -->
@@ -49,8 +53,10 @@
</modules>
<scm>
- <connection>scm:git:https://git.shibboleth.net/git/java-${project.artifactId}</connection>
- <developerConnection>scm:git:git at git.shibboleth.net:java-${project.artifactId}</developerConnection>
+ <connection>
+ scm:git:https://git.shibboleth.net/git/java-${project.artifactId}</connection>
+ <developerConnection>
+ scm:git:git at git.shibboleth.net:java-${project.artifactId}</developerConnection>
<url>https://git.shibboleth.net/view/?p=java-${project.artifactId}.git</url>
</scm>
@@ -59,7 +65,7 @@
<dependency>
<groupId>${slf4j.groupId}</groupId>
<artifactId>slf4j-api</artifactId>
- <scope>provided</scope>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
@@ -95,6 +101,12 @@
<artifactId>webauthn-server-core</artifactId>
<version>${yubico-webauthn.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.upokecenter</groupId>
+ <artifactId>cbor</artifactId>
+ <version>${com-upokecenter.version}</version>
+ <scope>runtime</scope>
+ </dependency>
<!-- Import Dependencies: Shib-Metadata/Attribute/Profile/Shared -->
<dependency>
<groupId>${shib-shared.groupId}</groupId>
@@ -140,9 +152,17 @@
<type>pom</type>
<scope>import</scope>
</dependency>
+ <!-- Jackson BOM -->
+ <dependency>
+ <groupId>com.fasterxml.jackson</groupId>
+ <artifactId>jackson-bom</artifactId>
+ <version>${jackson.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
-
+
<build>
<plugins>
<plugin>
@@ -150,12 +170,17 @@
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<links combine.children="append">
- <link>${javadoc.url}/java-shib-shared/${shib-shared.version}</link>
+ <link>
+ ${javadoc.url}/java-shib-shared/${shib-shared.version}</link>
<link>${javadoc.url}/java-opensaml/${opensaml.version}</link>
- <link>${javadoc.url}/java-shib-metadata/${shib-metadata.version}</link>
- <link>${javadoc.url}/java-shib-attribute/${shib-attribute.version}</link>
- <link>${javadoc.url}/java-shib-profile/${shib-profile.version}</link>
- <link>${javadoc.url}/java-identity-provider/${idp.version}</link>
+ <link>
+ ${javadoc.url}/java-shib-metadata/${shib-metadata.version}</link>
+ <link>
+ ${javadoc.url}/java-shib-attribute/${shib-attribute.version}</link>
+ <link>
+ ${javadoc.url}/java-shib-profile/${shib-profile.version}</link>
+ <link>
+ ${javadoc.url}/java-identity-provider/${idp.version}</link>
</links>
</configuration>
</plugin>
@@ -173,5 +198,5 @@
</configuration>
</plugin>
</plugins>
- </build>
+ </build>
</project>
diff --git a/webauthn-api/pom.xml b/webauthn-api/pom.xml
index 3a7d102..39d2b7e 100644
--- a/webauthn-api/pom.xml
+++ b/webauthn-api/pom.xml
@@ -25,11 +25,36 @@
distribution. -->
<!-- Provided dependencies -->
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-authn-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yubico</groupId>
+ <artifactId>webauthn-server-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-admin-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
new file mode 100644
index 0000000..3d8a1c3
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
@@ -0,0 +1,148 @@
+/*
+ * 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.plugin.authn.webauthn;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * <p>A base class for webauthn authentication related actions.</p>
+ *
+ * <p>In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
+ * and makes available the {@link WebAuthnAuthenticationContext}.</p>
+ *
+ * <p>Webauthn authentication action implementations should override the
+ * {@link #doExecute(ProfileRequestContext, AuthenticationContext, WebAuthnAuthenticationContext)}
+ * method.</p>
+ *
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
+ * @post <pre>AuthenticationContext.getSubcontext(WebAuthnAuthenticationContext.class) != null</pre>
+ */
+public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnAuthenticationAction.class);
+
+ /** Lookup strategy to locate the webauthn authentication context. */
+ @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
+
+ /** The Duo authentication Context.*/
+ @NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
+
+
+ /** Constructor.*/
+ protected AbstractWebAuthnAuthenticationAction() {
+ //prc -> ac -> dc
+ webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
+ compose(new ChildContextLookup<>(AuthenticationContext.class));
+ }
+
+
+ /**
+ * Set Duo authentication context lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setWebauthnContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+ checkSetterPreconditions();
+
+ webauthnContextLookupStrategy =
+ Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ return false;
+ }
+
+ webauthnContext = webauthnContextLookupStrategy.apply(profileRequestContext);
+ if (webauthnContext == null) {
+ log.warn("{} No Webauthn context returned by lookup strategy",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+
+ }
+ return doPreExecute(profileRequestContext, authenticationContext, webauthnContext);
+ }
+
+ /**
+ * Delegates to {@link #doExecute(ProfileRequestContext, AuthenticationContext,
+ * WebAuthnAuthenticationContext)} to perform the actual authentication. Implementations can not
+ * override this method.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param authenticationContext the current authentication context
+ */
+ @Override
+ protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ doExecute(profileRequestContext,authenticationContext, webauthnContext);
+ }
+
+ /**
+ * Performs this authentication action's pre-execute step. Default implementation just returns true.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param authenticationContext the current authentication context
+ * @param context the webauthn authentication context
+ *
+ * @return true iff execution should continue
+ */
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+ return true;
+ }
+
+ /**
+ * Performs this webauthn authentication action using the supplied webauthn context. Implementations
+ * should override this method.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param authenticationContext the current authentication context
+ * @param context the webauthn authentication context
+ */
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
new file mode 100644
index 0000000..0bd7cbb
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
@@ -0,0 +1,78 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+ at Immutable
+ at ThreadSafe
+ at JsonDeserialize(builder=PublicKeyCredential.Builder.class)
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public final class PublicKeyCredential {
+
+ @Nonnull private final String type;
+
+ @Nonnull private final String id;
+
+ @Nonnull private final Response response;
+
+ //no clientExtensionResults?
+
+
+ private PublicKeyCredential(Builder builder) {
+ this.type = builder.type;
+ this.id = builder.id;
+ this.response = builder.response;
+ }
+
+ /**
+ * Creates builder to build {@link PublicKeyCredential}.
+ * @return created builder
+ */
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder to build {@link PublicKeyCredential}.
+ */
+ @JsonPOJOBuilder(buildMethodName = "build",withPrefix = "with")
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static final class Builder {
+ private String type;
+
+ private String id;
+
+ private Response response;
+
+ private Builder() {
+ }
+
+ public Builder withType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder withId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder withResponse(Response response) {
+ this.response = response;
+ return this;
+ }
+
+ public PublicKeyCredential build() {
+ return new PublicKeyCredential(this);
+ }
+ }
+
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/Response.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/Response.java
new file mode 100644
index 0000000..90e38bb
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/Response.java
@@ -0,0 +1,64 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+ at Immutable
+ at ThreadSafe
+ at JsonDeserialize(builder=Response.Builder.class)
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class Response {
+
+ /** Base64 encoded attestation Object.*/
+ //TODO should be a byte array converted from the b64 in the JSON
+ @Nonnull private final String attestationObject;
+
+ /** JSON client data base64 encoded.*/
+ @Nonnull private final String clientDataJSON;
+
+ private Response(Builder builder) {
+ this.attestationObject = builder.attestationObject;
+ this.clientDataJSON = builder.clientDataJSON;
+ }
+
+ /**
+ * Creates builder to build {@link Response}.
+ * @return created builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder to build {@link Response}.
+ */
+ public static final class Builder {
+ private String attestationObject;
+
+ private String clientDataJSON;
+
+ private Builder() {
+ }
+
+ public Builder withAttestationObject(String attestationObject) {
+ this.attestationObject = attestationObject;
+ return this;
+ }
+
+ public Builder withClientDataJSON(String clientDataJSON) {
+ this.clientDataJSON = clientDataJSON;
+ return this;
+ }
+
+ public Response build() {
+ return new Response(this);
+ }
+ }
+
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java
new file mode 100644
index 0000000..de36c42
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java
@@ -0,0 +1,39 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A client that manages the entire webauthn authentication ceremony,
+ * see {@link https://www.w3.org/TR/webauthn/#authentication-ceremony}.
+ *
+ * <p>Implementations must be thread-safe</p>
+ */
+//TODO integrate this with the CredentialValidator interface?
+ at ThreadSafe
+public interface WebauthnAuthenticationClient {
+
+
+ /**
+ * Create a JSON serialized PublicKeyCredentialRequestOptions.
+ *
+ * @param username the username of the username that has been pre-identified. Can be null
+ * if no user has been identified, and the IdP is requesting the client discover
+ * the credential.
+ * @return a JSON serialized PublicKeyCredentialRequestOptions object. Can be null if
+ * one could not be created.
+ */
+ //TODO how do we guarantee this is JSON, or just return the Yubico object?
+ @Nullable String createAuthenticationRequest(@Nullable final String username);
+
+
+ /**
+ * Validate the Authenticator Assertion Response.
+ *
+ * @param jsonAssertionResponse the JSON representation of the assertion response.
+ *
+ * @return true if the assertion was verified successfully, false otherwise.
+ */
+ boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse);
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java
new file mode 100644
index 0000000..7e79120
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java
@@ -0,0 +1,51 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+/**
+ * An exception to signal an error condition during execution or creation of a Webauthn client.
+ */
+public class WebauthnAuthenticationClientException extends Exception {
+
+ /** Serial UID. */
+ private static final long serialVersionUID = -2380145079984333546L;
+
+ /**
+ * Constructor.
+ *
+ */
+ public WebauthnAuthenticationClientException() {
+ super();
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param cause exception to be wrapped by this one
+ */
+ public WebauthnAuthenticationClientException(final String message, final Throwable cause) {
+ super(message, cause);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public WebauthnAuthenticationClientException(final String message) {
+ super(message);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause exception to be wrapped by this one
+ */
+ public WebauthnAuthenticationClientException(final Throwable cause) {
+ super(cause);
+
+ }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
new file mode 100644
index 0000000..3547639
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
@@ -0,0 +1,20 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Abstract factory for creating singleton {@link WebauthnAuthenticationClient} instances.
+ */
+public interface WebauthnAuthenticationClientFactory {
+
+
+ /**
+ * Create an {@link WebauthnAuthenticationClient} instance.
+ *
+ * @return the client, never {@code nul}.
+ *
+ * @throws WebauthnAuthenticationClientException if there is an errtor creating the client.
+ */
+ @Nonnull WebauthnAuthenticationClient createInstance() throws WebauthnAuthenticationClientException;
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnCredentialRegistrationFlowDescriptor.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnCredentialRegistrationFlowDescriptor.java
new file mode 100644
index 0000000..c8ff1f2
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnCredentialRegistrationFlowDescriptor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.admin;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.admin.BasicAdministrativeFlowDescriptor;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * And administrative flow descriptor to register user WebAuthn credentials.
+ */
+public class WebAuthnCredentialRegistrationFlowDescriptor extends BasicAdministrativeFlowDescriptor {
+
+ /** Profile ID. */
+ @Nonnull @NotEmpty public static final String PROFILE_ID =
+ "http://shibboleth.net/ns/profiles/webauthn/register-credential";
+
+ /**
+ * Constructor.
+ */
+ public WebAuthnCredentialRegistrationFlowDescriptor() {
+ super(PROFILE_ID);
+ setNonBrowserSupported(false);
+ }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
new file mode 100644
index 0000000..1f51f87
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
@@ -0,0 +1,200 @@
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.logic.Constraint;
+
+
+
+ at NotThreadSafe
+public final class WebAuthnAuthenticationContext extends BaseContext {
+
+ @Nullable private byte[] serverChallenge;
+
+ /** A registration public key credential.*/
+ @Nullable
+ private PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> publicKeyCredential;
+
+ /** The credential public key encoded in COSE_Key format.*/
+ @Nullable private byte[] publicKey;
+
+ /** The credential identifier generated by the authenticator.*/
+ @Nullable private byte[] credentialId;
+
+ /** The original username. */
+ @Nullable private String username;
+
+ /** In a new context? TODO. But for now an existing credId if found.*/
+ @Nullable private byte[] existingCredentialId;
+
+ /** In a new context? TODO. The existing credential public key encoded in COSE_Key format if found.*/
+ @Nullable private byte[] existingPublicKey;
+
+ /** An assertion response that is the result of an authentication.*/
+ @Nullable
+ private PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs> assertionResponse;
+
+ /**
+ * Set the server challenge which the client authenticator needs to sign.
+ *
+ * @param challenge the challenge, must not be empty and must be minimum 16 bytes long.
+ *
+ * @return this context.
+ */
+ @Nonnull public WebAuthnAuthenticationContext setServerChallenge(@Nonnull final byte[] challenge) {
+ Constraint.isNotEmpty(challenge,"Challenge can not be null or empty");
+ Constraint.isGreaterThan(16, challenge.length, "Challenge must be at least 16 bytes");
+ serverChallenge = challenge;
+ return this;
+ }
+
+ /**
+ * Get the assertion response as a result of authentication.
+ *
+ * @return the assertion.
+ */
+ @Nullable public PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs>
+ getAssertionResponse() {
+ return assertionResponse;
+ }
+
+ @Nonnull public WebAuthnAuthenticationContext setAssertionResponse(@Nullable final
+ PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs> assertion) {
+ assertionResponse = assertion;
+ return this;
+ }
+
+ /**
+ * Gets the username.
+ *
+ * @return the username
+ */
+ @Nullable public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Sets the username and resets the transformed version to be identical.
+ *
+ * @param name the username
+ *
+ * @return this context
+ */
+ @Nonnull public WebAuthnAuthenticationContext setUsername(@Nullable final String name) {
+ username = name;
+ //transformedUsername = name;
+ return this;
+ }
+
+ /**
+ * Get the server challenge for the authenticator to sign.
+ *
+ * @return the server challenge.
+ */
+ @Nullable public byte[] getServerChallenge() {
+ return serverChallenge;
+ }
+
+ /**
+ * Get the public key in COSE_Key format.
+ *
+ * @return the public key.
+ */
+ @Nullable public byte[] getPublicKey() {
+ return publicKey;
+ }
+
+ /**
+ * Set the public key, as a byte array, in COSE_Key format.
+ *
+ * @param key the public key in COSE_Key format.
+ */
+ public WebAuthnAuthenticationContext setPublicKey(@Nonnull final byte[] key) {
+ publicKey = Constraint.isNotNull(key, "Public key can not be null");
+ return this;
+ }
+
+ /**
+ * Get the existing public key in COSE_Key format.
+ *
+ * @return the public key.
+ */
+ @Nullable public byte[] getExsitingPublicKey() {
+ return publicKey;
+ }
+
+ /**
+ * Set the existing public key, as a byte array, in COSE_Key format.
+ *
+ * @param key the public key in COSE_Key format.
+ */
+ public WebAuthnAuthenticationContext setExistingPublicKey(@Nonnull final byte[] key) {
+ publicKey = Constraint.isNotNull(key, "Public key can not be null");
+ return this;
+ }
+
+ /**
+ * Get the existing credential Id.
+ *
+ * @return the credential Id.
+ */
+ public byte[] getExistingCredentialId() {
+ return credentialId;
+ }
+
+ /**
+ * Set the existing credential Id.
+ *
+ * @param id the credential Id.
+ */
+ public WebAuthnAuthenticationContext setExistingCredentialId(@Nonnull final byte[] id) {
+ credentialId = Constraint.isNotNull(id, "Credential ID can not be null");
+ return this;
+ }
+
+ /**
+ * Get the credential Id.
+ *
+ * @return the credential Id.
+ */
+ public byte[] getCredentialId() {
+ return credentialId;
+ }
+
+ /**
+ * Set the credential Id.
+ *
+ * @param id the credential Id.
+ */
+ public WebAuthnAuthenticationContext setCredentialId(@Nonnull final byte[] id) {
+ credentialId = Constraint.isNotNull(id, "Credential ID can not be null");
+ return this;
+ }
+
+ @Nullable
+ public PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> getPublicKeyCredential() {
+ return publicKeyCredential;
+ }
+
+ public void setPublicKeyCredential(@Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential) {
+ publicKeyCredential = Constraint.isNotNull(credential, "Public Key Credential can not be null or empty");
+ }
+
+ //TODO throw in a context, this could be null (which is bad here?)
+ @SuppressWarnings("null")
+ @Nullable public String getServerChallengeBase64() throws EncodingException {
+ return Base64Support.encode(serverChallenge, false);
+ }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnPublicKeyCredentialRecord.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnPublicKeyCredentialRecord.java
new file mode 100644
index 0000000..628cac8
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnPublicKeyCredentialRecord.java
@@ -0,0 +1,49 @@
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.shared.logic.Constraint;
+
+ at Immutable
+ at ThreadSafe
+//TODO does this look like a storage record?
+public final class WebAuthnPublicKeyCredentialRecord {
+
+ /** The credential public key encoded in COSE_Key format.*/
+ @Nonnull private final byte[] publicKey;
+
+ /** The credential identifier generated by the authenticator.*/
+ @Nullable private final byte[] credentialId;
+
+ /**
+ * Constructor.
+ *
+ * @param key the public key to store.
+ */
+ public WebAuthnPublicKeyCredentialRecord(@Nonnull final byte[] key, @Nonnull final byte[] credId) {
+ publicKey = Constraint.isNotNull(key, "Public key can not be null");
+ credentialId = Constraint.isNotNull(credId, "Credential ID can not be null");
+ }
+
+ /**
+ * Get the COSE_Key formatted public key.
+ *
+ * @return the public key in COSE_Key format.
+ */
+ @Nullable public byte[] getPublicKey() {
+ return publicKey;
+ }
+
+ /**
+ * Get the credential ID.
+ *
+ * @return the credential ID.
+ */
+ @Nullable public byte[] getCredentialId() {
+ return credentialId;
+ }
+
+}
diff --git a/webauthn-impl/pom.xml b/webauthn-impl/pom.xml
index 7c0a6bc..e9375cd 100644
--- a/webauthn-impl/pom.xml
+++ b/webauthn-impl/pom.xml
@@ -1,4 +1,5 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@@ -9,21 +10,38 @@
</parent>
<artifactId>idp-plugin-webauthn-impl</artifactId>
- <packaging>jar</packaging>
+ <packaging>jar</packaging>
<name>Shibboleth IdP :: Plugins :: WebAuthn Login Flow Impl</name>
<description>IdP WebAuthn plugin implementation.</description>
-
+
<properties>
- <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+ <checkstyle.configLocation>
+ ${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
<automatic.module.name>net.shibboleth.idp.plugin.authn.webauthn.impl</automatic.module.name>
</properties>
-
+
<dependencies>
- <!-- Compile time intra-project dependencies -->
+ <!-- Compile time intra-project dependencies -->
<dependency>
<groupId>net.shibboleth.idp.plugin.authn</groupId>
<artifactId>idp-plugin-webauthn-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.yubico</groupId>
+ <artifactId>webauthn-server-core</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency> <!-- note when we move to more yubico libs this will need to be runtime -->
+ <groupId>com.upokecenter</groupId>
+ <artifactId>cbor</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <!-- Runtime dependencies -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.dataformat</groupId>
+ <artifactId>jackson-dataformat-cbor</artifactId>
+ <scope>runtime</scope>
+ </dependency>
<!-- Service API and Plugin Description dependencies -->
<dependency>
<groupId>${idp.groupId}</groupId>
@@ -40,8 +58,32 @@
<artifactId>shib-profile-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
<!-- Provided dependencies -->
-
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <scope>provided</scope>
+ </dependency>
<!-- Test dependencies -->
<dependency>
<groupId>${idp.groupId}</groupId>
@@ -57,7 +99,7 @@
<groupId>${shib-attribute.groupId}</groupId>
<artifactId>shib-attribute-filter-impl</artifactId>
<scope>test</scope>
- </dependency>
+ </dependency>
<dependency>
<groupId>${shib-shared.groupId}</groupId>
<artifactId>shib-attribute-filter-spring</artifactId>
@@ -67,12 +109,12 @@
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-messaging-impl</artifactId>
<scope>test</scope>
- </dependency>
+ </dependency>
<dependency>
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-core-impl</artifactId>
<scope>test</scope>
- </dependency>
+ </dependency>
<dependency>
<groupId>${shib-shared.groupId}</groupId>
<artifactId>shib-security</artifactId>
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
new file mode 100644
index 0000000..be16e4f
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
@@ -0,0 +1,100 @@
+/*
+ * 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.client.impl;
+
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.yubico.webauthn.AssertionRequest;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.StartAssertionOptions;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Yuibico version of the {@link WebauthnAuthenticationClient}.
+ *
+ * <p>Thread-safe, only a single instance is required.</p>
+ */
+ at ThreadSafe
+public class YubicoWebauthnAuthenticationClient implements WebauthnAuthenticationClient {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(YubicoWebauthnAuthenticationClient.class);
+
+ /** Information pertaining to the relying party.*/
+ @Nonnull private final RelyingParty rp;
+
+ /** The JSON object mapper used to JSONify webauthn objects. */
+ @Nonnull private final ObjectMapper om;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param relyingParty Information pertaining to the relying party.
+ * @param mapper the JSON object mapper.
+ */
+ public YubicoWebauthnAuthenticationClient(@Nonnull final RelyingParty relyingParty,
+ @Nonnull final ObjectMapper mapper) {
+ rp = Constraint.isNotNull(relyingParty, "The reyling party configuration can not be null");
+ om = Constraint.isNotNull(mapper, "Object mapper can not be null");
+ }
+
+ @Override
+ public String createAuthenticationRequest(@Nullable final String username) {
+ try {
+ //set default to preferred.
+ UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
+ if (username == null) {
+ //then require user verification? makes sense, but is that part of the spec?
+ userVerificationRequirement = UserVerificationRequirement.REQUIRED;
+ }
+ final AssertionRequest assertion =
+ rp.startAssertion(StartAssertionOptions.builder()
+ .username(Optional.ofNullable(username))
+ .userVerification(userVerificationRequirement)
+ .build());
+ final ObjectWriter writer = om.writerWithDefaultPrettyPrinter();
+ return writer.writeValueAsString(assertion);
+ }
+ catch (final JsonProcessingException e) {
+ log.error("Could not construct FIDO2 authentication request for '{}'",username,e);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse) {
+
+ if (jsonAssertionResponse == null || jsonAssertionResponse.isEmpty()) {
+ log.warn("JSON Assertion Response is either null or empty, authentication can not be validated");
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
new file mode 100644
index 0000000..a816a1c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
@@ -0,0 +1,208 @@
+/*
+ * 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.client.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.springframework.beans.factory.FactoryBean;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+
+import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.DummyCredentialRepository;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Spring factory beans for creating a {@link YubicoWebauthnAuthenticationClient}.
+ */
+//TODO do we need a factory abstract for this type of initialisation e.g. not runtime.
+ at ThreadSafe
+public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
+ implements FactoryBean<WebauthnAuthenticationClient> {
+
+ /** The relying party identifier.*/
+ @GuardedBy("this") @Nullable private String relyingPartyId;
+
+ /** The relying party name.*/
+ @GuardedBy("this") @Nullable private String relyingPartyName;
+
+ /** Allow unrestricted origin ports? Default is false.*/
+ @GuardedBy("this") private boolean allowOriginPort;
+
+ /** Allow subdomains in origins? Default is false.*/
+ @GuardedBy("this") private boolean allowOriginSubdomain;
+
+ /** The JSON object mapper used to JSONify webauthn objects. */
+ @GuardedBy("this") @Nullable private ObjectMapper om;
+
+ /** Constructor.*/
+ public YubicoWebauthnClientFactory() {
+ allowOriginPort = false;
+ allowOriginSubdomain = false;
+ }
+
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (relyingPartyId == null) {
+ throw new ComponentInitializationException("relyingPartyId cannot be null");
+ }
+ if (relyingPartyName == null) {
+ throw new ComponentInitializationException("relyingPartyName cannot be null");
+ }
+ if (om == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+
+ }
+
+
+ @Override
+ public WebauthnAuthenticationClient getObject() throws Exception {
+ final RelyingParty rp = RelyingParty.builder().identity(
+ RelyingPartyIdentity
+ .builder()
+ .id(getRelyingPartyId())
+ .name(getRelyingPartyName())
+ .build()).credentialRepository(new DummyCredentialRepository())
+ .allowOriginPort(isAllowOriginPort())
+ .allowOriginSubdomain(isAllowOriginSubdomain())
+ .build();
+
+ return new YubicoWebauthnAuthenticationClient(rp, getObjectMapper());
+ }
+
+ @Override
+ public Class<?> getObjectType() {
+ return WebauthnAuthenticationClient.class;
+ }
+
+ @Override
+ public boolean isSingleton() {
+ return true;
+ }
+
+ /**
+ * Set the object mapper.
+ *
+ * @param objectMapper the object mapper.
+ */
+ public synchronized void setObjectMapper(@Nonnull final ObjectMapper objectMapper) {
+ om = Constraint.isNotNull(objectMapper, "Object mapper can not be null");
+ }
+
+ /**
+ * Get the object mapper.
+ *
+ * @return the objectMapper;
+ */
+ private synchronized ObjectMapper getObjectMapper() {
+ return om;
+ }
+ /**
+ * Set the relying party identifier.
+ *
+ * @param relyingPartyIdentifier the relying party identifier.
+ */
+ public synchronized void setRelyingPartyId(@Nonnull final String relyingPartyIdentifier) {
+
+ relyingPartyId = Constraint.isNotNull(relyingPartyIdentifier,"You must set a relying party ID");
+ }
+
+ /**
+ * Get the relying party identifier.
+ * <p>
+ * The RP ID must be equal to the origin's effective domain, or a registrable
+ * domain suffix of the origin's effective domain.
+ * </p>
+ *
+ * @return the relying party identifier.
+ */
+ @Nullable private synchronized String getRelyingPartyId() {
+ return relyingPartyId;
+ }
+
+ /**
+ * Set the relying party name.
+ *
+ * @param relyingPartyIdentifier the relying party name.
+ */
+ public synchronized void setRelyingPartyName(@Nonnull final String name) {
+
+ relyingPartyName = Constraint.isNotNull("You must set a relying party name", name);
+ }
+
+ /**
+ * Get the relying party name.
+ *
+ * @return the relying party name.
+ */
+ @Nullable private synchronized String getRelyingPartyName() {
+
+ return relyingPartyName;
+ }
+
+ /**
+ * Is origin port allowed?
+ *
+ * @return allow origin port?
+ */
+ private synchronized boolean isAllowOriginPort() {
+
+ return allowOriginPort;
+ }
+
+ /**
+ * Allow unrestricted origin ports?
+ *
+ * @param allow allow origin port.
+ */
+ //TODO Javadoc
+ public synchronized void setAllowOriginPort(final boolean allow) {
+
+ allowOriginPort = allow;
+ }
+
+ /**
+ * Are subdomains allowed?
+ *
+ * @return allow subdomains?
+ */
+ private synchronized boolean isAllowOriginSubdomain() {
+
+ return allowOriginSubdomain;
+ }
+
+ /**
+ * Set if subdomains are allowed.
+ *
+ * @param allow are subdomains allowed?
+ */
+ public synchronized void setAllowOriginSubdomain(final boolean allow) {
+
+ allowOriginSubdomain = allow;
+ }
+
+
+
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
new file mode 100644
index 0000000..d197a5d
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
@@ -0,0 +1,72 @@
+/*
+ * 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.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Create a public key credential request...
+ */
+public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialRequestOptions.class);
+
+ /** The Webauthn client.*/
+ @NonnullAfterInit private WebauthnAuthenticationClient webauthnClient;
+
+ /**
+ * Set the WebAuthn client to use.
+ *
+ * @param client the client.
+ */
+ public void setWebauthnClient(@Nonnull final WebauthnAuthenticationClient client) {
+ checkSetterPreconditions();
+ webauthnClient = Constraint.isNotNull(client, "WebAuthn Client can not be null");
+ }
+
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (webauthnClient == null) {
+ throw new ComponentInitializationException("Webauthn client cannot be null");
+ }
+
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ final String pkCredRequestOptions = webauthnClient.createAuthenticationRequest(context.getUsername());
+ log.debug("Created PublicKeyCredentialRequestOptions '{}'",pkCredRequestOptions);
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java
new file mode 100644
index 0000000..859e2b3
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that derives the PublicKeyCredential from a form parameter.
+ */
+public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+
+ /** Default token code field name. */
+ @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "publicKeyAssertion";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyAssertionFromFormRequest.class);
+
+ /** Name of header. */
+ @NonnullAfterInit @NotEmpty private String fieldName;
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** Constructor. */
+ public ExtractPublicKeyAssertionFromFormRequest() {
+ fieldName = DEFAULT_FIELD_NAME;
+ }
+
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("The objectMapper cannot be null");
+ }
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper}.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /**
+ * Set the name of the field to examine.
+ *
+ * @param field field name
+ */
+ public void setFieldName(@Nonnull @NotEmpty final String field) {
+ checkSetterPreconditions();
+
+ fieldName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Field name cannot be null or empty");
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request == null) {
+ log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ final String pkCredJson = extractPublicKeyCredential(request);
+ log.trace("Public Key Assertion in JSON is '{}'",pkCredJson);
+ if (pkCredJson == null) {
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ try {
+ final PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs> pkCred =
+ objectMapper.readValue(pkCredJson,
+ new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs>>() {});
+ log.debug("Client Data '{}'",pkCred.getResponse().getClientData());
+ log.debug("Signature '{}'",pkCred.getResponse().getSignature());
+ context.setAssertionResponse(pkCred);
+ } catch (final JsonProcessingException e) {
+ log.debug("{} Can not extract public key credential response", getLogPrefix(),e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+
+ }
+
+ @Nullable private String extractPublicKeyCredential(@Nonnull final HttpServletRequest httpRequest) {
+ return httpRequest.getParameter(fieldName);
+ }
+
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
new file mode 100644
index 0000000..d5577b7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that derives the PublicKeyCredential from a form parameter.
+ */
+public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+
+ /** Default token code field name. */
+ @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "publicKeyCredential";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialFromFormRequest.class);
+
+ /** Name of header. */
+ @Nonnull @NotEmpty private String fieldName;
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** Constructor. */
+ public ExtractPublicKeyCredentialFromFormRequest() {
+ fieldName = DEFAULT_FIELD_NAME;
+ }
+
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("The objectMapper cannot be null");
+ }
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper}.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /**
+ * Set the name of the field to examine.
+ *
+ * @param field field name
+ */
+ public void setFieldName(@Nonnull @NotEmpty final String field) {
+ checkSetterPreconditions();
+
+ fieldName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Field name cannot be null or empty");
+ }
+
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request == null) {
+ log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ final String pkCredJson = extractPublicKeyCredential(request);
+ log.trace("Public Key Credential in JSON is '{}'",pkCredJson);
+ if (pkCredJson == null) {
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ try {
+ final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> pkCred =
+ objectMapper.readValue(pkCredJson,
+ new TypeReference<PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs>>() {});
+ log.debug("Client Data '{}'",pkCred.getResponse().getClientData());
+ log.debug("Attestation Data '{}'",pkCred.getResponse().getAttestation());
+ context.setPublicKeyCredential(pkCred);
+ } catch (final JsonProcessingException e) {
+ log.debug("{} Can not extract public key credential response", getLogPrefix(),e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+
+ }
+
+ @Nullable private String extractPublicKeyCredential(@Nonnull final HttpServletRequest httpRequest) {
+ return httpRequest.getParameter(fieldName);
+ }
+
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
new file mode 100644
index 0000000..bd874f2
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
@@ -0,0 +1,82 @@
+/*
+ * 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.plugin.authn.webauthn.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action to generate an appropriate server challenge for use.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ * @post See above.
+ */
+public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateServerChallenge.class);
+
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ try {
+ //TODO check the spec, this should be a a buffersource, maybe the JS is generating it as one
+ final byte[] challenge = generateChallenge();
+ log.trace("Generated challenge {}",challenge);
+ context.setServerChallenge(challenge);
+
+ } catch (final NoSuchAlgorithmException e) {
+ log.error("Could not generate a challenge",e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ }
+ }
+
+ /**
+ * Generate a 32 bytes randomized challenge of sufficient entropy. Must be at least 16 bytes long.
+ *
+ * @return the challenge in bytes
+ *
+ * @throws NoSuchAlgorithmException if no secure random algorithm is available
+ */
+ @Nonnull private byte[] generateChallenge() throws NoSuchAlgorithmException {
+ final byte[] bytes = new byte[32];
+ SecureRandom.getInstanceStrong().nextBytes(bytes);
+ log.trace("Generated '{}' byte challenge",bytes.length);
+ return bytes;
+ }
+
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
new file mode 100644
index 0000000..3c33779
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
@@ -0,0 +1,127 @@
+/*
+ * 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.impl;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action to create (or lookup) and populate the {@link WebauthnAuthenticationContext}
+ * with the ... FIXME appropriate for this request.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ * @post See above.
+ */
+public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebauthnAuthenticationContext.class);
+
+ /** Strategy used to locate or create the {@link WebauthnAuthenticationContext} to populate. */
+ @Nonnull
+ private final Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnAuthContextCreationStrategy;
+
+ /** Lookup strategy for username to match against Duo identity. */
+ @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
+
+ /** Is the username required?*/
+ private Predicate<ProfileRequestContext> usernameRequiredPredicate;
+
+
+ /** Constructor.*/
+ public PopulateWebauthnAuthenticationContext() {
+ //default creates webauthn authentication context under authentication context.
+ webauthnAuthContextCreationStrategy =
+ new ChildContextLookup<>(WebAuthnAuthenticationContext.class, true).
+ compose(new ChildContextLookup<>(AuthenticationContext.class));
+
+ usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
+ usernameRequiredPredicate = PredicateSupport.alwaysTrue();
+ }
+
+ /**
+ * @param flag The usernameRequired to set.
+ */
+ public void setUsernameRequired(final boolean flag) {
+ checkSetterPreconditions();
+ usernameRequiredPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+ }
+
+ /**
+ * @param usernameRequiredPredicate The usernameRequiredPredicate to set.
+ */
+ public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
+ checkSetterPreconditions();
+ usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
+ }
+
+ /**
+ * Set the lookup strategy to use for the username to match against Duo identity.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUsernameLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+
+ usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+
+ final WebAuthnAuthenticationContext context = webauthnAuthContextCreationStrategy.apply(profileRequestContext);
+ if (context == null) {
+ log.error("{} Error creating WebauthnAuthenticationContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ final String username = usernameLookupStrategy.apply(profileRequestContext);
+ if (username == null && usernameRequiredPredicate.test(profileRequestContext)) {
+ log.error("{} Error creating WebauthnAuthenticationContext, no username found", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
+
+ log.debug("Created Webauthn authentication context");
+ }
+
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/UsernameFromAuthenticationContextLookupStrategy.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/UsernameFromAuthenticationContextLookupStrategy.java
new file mode 100644
index 0000000..417473c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/UsernameFromAuthenticationContextLookupStrategy.java
@@ -0,0 +1,55 @@
+/*
+ * 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.impl;
+
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+
+/**
+ * Take the UsernamePrincipal from the authentication context iff there is only one. Return {@code null} otherwise.
+ */
+public class UsernameFromAuthenticationContextLookupStrategy implements Function<ProfileRequestContext, String> {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String apply(@Nullable final ProfileRequestContext input) {
+
+ if (input != null) {
+ final AuthenticationContext authnContext = input.getSubcontext(AuthenticationContext.class);
+ if (authnContext != null) {
+ final AuthenticationResult result = authnContext.getAuthenticationResult();
+ if (result != null) {
+ final Subject subject = result.getSubject();
+ final Set<UsernamePrincipal> usernamePrincipals = subject.getPrincipals(UsernamePrincipal.class);
+ if (usernamePrincipals.size() == 1) {
+ return usernamePrincipals.iterator().next().getName();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
new file mode 100644
index 0000000..7c86f77
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
@@ -0,0 +1,73 @@
+/*
+ * 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.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Validate the public key registration attempt.
+ */
+public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(ValidatePublicKeyCredential.class);
+
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkCred =
+ context.getPublicKeyCredential();
+
+ if (pkCred == null) {
+ log.error("{} public key credential was null", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ // Validate the registration
+
+ // If valid. Add back to context
+ log.info("Public Key Registration was valid");
+ //now set the credential data e.g. created public key and keyID onto the context.
+ final ByteArray credId = pkCred.getResponse().getAttestation().getAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialId();
+ final ByteArray credPublicKey = pkCred.getResponse().getAttestation().getAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialPublicKey();
+ context.setCredentialId(credId.getBytes());
+ context.setPublicKey(credPublicKey.getBytes());
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
new file mode 100644
index 0000000..3cfb69c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
@@ -0,0 +1,81 @@
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.security.auth.Subject;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AbstractValidationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+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.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * See https://w3c.github.io/webauthn/#sctn-verifying-assertion
+ */
+public class ValidateWebAuthnAssertion extends AbstractValidationAction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(ValidateWebAuthnAssertion.class);
+
+ /** Strategy used to locate or create the {@link WebAuthnAuthenticationContext} to populate. */
+ @Nonnull
+ private final Function<ProfileRequestContext, WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
+
+ /** The webauthn authentication context. */
+ @NonnullBeforeExec private WebAuthnAuthenticationContext context;
+
+ /** Constructor. */
+ public ValidateWebAuthnAssertion() {
+ webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class)
+ .compose(new ChildContextLookup<>(AuthenticationContext.class));
+
+ }
+
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ context = webauthnContextLookupStrategy.apply(profileRequestContext);
+ if (context == null) {
+ log.warn("{} No WebAuthn context returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+
+ }
+ return true;
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ context.setUsername("jblogs");
+ buildAuthenticationResult(profileRequestContext, authenticationContext);
+
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+ super.buildAuthenticationResult(profileRequestContext, authenticationContext);
+ }
+
+ @Override
+ protected Subject populateSubject(@Nonnull final Subject subject) {
+ final String username = context.getUsername();
+ assert username != null;
+ subject.getPrincipals().add(new UsernamePrincipal(username));
+ return subject;
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java
new file mode 100644
index 0000000..9b60af7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.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.storage.impl;
+
+import java.util.Optional;
+import java.util.Set;
+
+import com.yubico.webauthn.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
+/** A dummy CredentialRepository for testing.*/
+public class DummyCredentialRepository implements CredentialRepository{
+
+ @Override
+ public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
+ return Set.of(RegisteredCredential.builder()
+ .credentialId(new ByteArray("credid".getBytes()))
+ .userHandle(new ByteArray("userhandle".getBytes()))
+ .publicKeyCose(new ByteArray("publickeycose".getBytes()))
+ .signatureCount(0)
+ .build());
+ }
+
+ @Override
+ public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
+ return Optional.of(RegisteredCredential.builder()
+ .credentialId(new ByteArray("credid".getBytes()))
+ .userHandle(new ByteArray("userhandle".getBytes()))
+ .publicKeyCose(new ByteArray("publickeycose".getBytes()))
+ .signatureCount(0)
+ .build());
+ }
+
+ @Override
+ public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
+ return Optional.of("jdoe");
+ }
+
+ @Override
+ public Optional<ByteArray> getUserHandleForUsername(final String username) {
+ return Optional.of(new ByteArray("credid".getBytes()));
+ }
+
+ @Override
+ public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
+ return Set.of(
+ PublicKeyCredentialDescriptor.builder()
+ .id(new ByteArray("credid".getBytes()))
+ .build());
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
new file mode 100644
index 0000000..5952ac9
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
@@ -0,0 +1,161 @@
+/*
+ * 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.plugin.authn.webauthn.storage.impl;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.codec.binary.Hex;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageCapabilities;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageSerializer;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnPublicKeyCredentialRecord;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+//TODO extend appropriate class to get WebauthnAuthenticationContext
+public class StorePublicKeyCredential extends AbstractWebAuthnAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(StorePublicKeyCredential.class);
+
+ /** Backing service. */
+ @NonnullAfterInit
+ private StorageService storageService;
+
+ /** Storage record serializer. */
+ @Nonnull
+ private final StorageSerializer<WebAuthnPublicKeyCredentialRecord> serializer;
+
+ /** Constructor. */
+ public StorePublicKeyCredential() {
+ serializer = new WebauthnPublicKeyCredentialStorageSerializer();
+ }
+
+ /**
+ * Set the {@link StorageService} back-end to use.
+ *
+ * @param storage
+ * the back-end to use
+ */
+ public void setStorageService(@Nonnull final StorageService storage) {
+ checkSetterPreconditions();
+
+ storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+ final StorageCapabilities caps = storageService.getCapabilities();
+ Constraint.isTrue(caps.isServerSide(), "StorageService cannot be client-side");
+ }
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (storageService == null) {
+ throw new ComponentInitializationException("StorageService cannot be null");
+ }
+ }
+
+ // TODO maybe not needed.
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+ return true;
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ final String username = context.getUsername();
+ final byte[] publicKey = context.getPublicKey();
+ if (username == null) {
+ log.error("Unable to find username in registration response");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ if (publicKey == null) {
+ log.error("Unable to find public key in registration response");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ final byte[] credentialId = context.getCredentialId();
+ if (credentialId == null) {
+ log.error("Unable to find credential Id in registration response");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ final String hexEncodedCredentialId = Hex.encodeHexString(credentialId);
+ if (hexEncodedCredentialId == null) {
+ log.error("Unable to encode credential Id in registration response");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+
+ // TODO one key per user, and overwrite the old one, this is obviously all
+ // wrong?
+ try {
+ final StorageRecord<Object> existing = storageService.read("webauthn-keys", username);
+ if (existing != null) {
+ final boolean updated = storageService.update("webauthn-keys", username,
+ new WebAuthnPublicKeyCredentialRecord(publicKey, credentialId), serializer, null);
+ if (updated) {
+ log.debug("Updated webauthn-keys for user {}", context.getUsername());
+ } else {
+ log.error("Unable to update public key registration");
+ // TODO Event type is wrong
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ } else {
+ final boolean created = storageService.create("webauthn-keys", username,
+ new WebAuthnPublicKeyCredentialRecord(publicKey, credentialId), serializer, null);
+ if (created) {
+ log.debug("Stored webauthn-keys for user {}", context.getUsername());
+ } else {
+ log.error("Unable to store public key registration");
+ // TODO Event type is wrong
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ }
+
+ } catch (final IOException e) {
+ log.error("Unable to store public key registration", e);
+ // TODO Event type is wrong
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/WebauthnPublicKeyCredentialStorageSerializer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/WebauthnPublicKeyCredentialStorageSerializer.java
new file mode 100644
index 0000000..38a2541
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/WebauthnPublicKeyCredentialStorageSerializer.java
@@ -0,0 +1,59 @@
+/*
+ * 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.storage.impl;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.opensaml.storage.StorageSerializer;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnPublicKeyCredentialRecord;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/**
+ * Serialize the WebauthnPublicKeyCredentialRecord to a string using Hex encoding.
+ */
+public class WebauthnPublicKeyCredentialStorageSerializer extends AbstractInitializableComponent
+ implements StorageSerializer<WebAuthnPublicKeyCredentialRecord> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(WebauthnPublicKeyCredentialStorageSerializer.class);
+
+ @Override
+ public String serialize(final WebAuthnPublicKeyCredentialRecord instance) throws IOException {
+ return Hex.encodeHexString(instance.getPublicKey())+"|"+Hex.encodeHexString(instance.getCredentialId());
+ }
+
+ @Override
+ public WebAuthnPublicKeyCredentialRecord deserialize(final long version, final String context, final String key, final String value,
+ final Long expiration) throws IOException {
+ try {
+ //FIXME what about nulls etc.
+ log.debug("Value {}",value);
+ final String[] components = value.split("\\|");
+ log.debug("Split pk cred {}:{}",components[0],components[1]);
+ return new WebAuthnPublicKeyCredentialRecord(Hex.decodeHex(components[0]),Hex.decodeHex(components[1]));
+ } catch (final DecoderException e) {
+ throw new IOException("Error converting public key hex to bytes",e);
+ }
+ }
+
+}
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index dd18f8b..c8209e5 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -15,6 +15,30 @@
removed if not needed
-->
+ <!-- Authentication flow selection flow -->
+ <bean id="authn/WebAuthnFlowChooser" parent="shibboleth.AuthenticationFlow"
+ p:order="1000"
+ p:nonBrowserSupported="false"
+ p:passiveAuthenticationSupported="false"
+ p:forcedAuthenticationSupported="true"
+ p:lifetime="%{idp.authn.webauthn.lifetime:%{idp.authn.defaultLifetime:PT1H}}"
+ p:inactivityTimeout="%{idp.authn.webauthn.inactivityTimeout:%{idp.authn.defaultTimeout:PT30M}}"
+ p:reuseCondition-ref="#{'%{idp.authn.webauthn.reuseCondition:shibboleth.Conditions.TRUE}'.trim()}"
+ p:activationCondition-ref="#{'%{idp.authn.webauthn.activationCondition:shibboleth.Conditions.TRUE}'.trim()}">
+ <property name="supportedPrincipals">
+ <list>
+ <bean parent="shibboleth.SAML2AuthnContextClassRef"
+ c:classRef="class-ref" />
+ <bean parent="shibboleth.SAML1AuthenticationMethod"
+ c:method="auth-ref" />
+ </list>
+ </property>
+ <property name="supportedPrincipalsByString">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.authn.webauthn.supportedPrincipals:}'.trim()}" />
+ </property>
+ </bean>
+
<bean id="authn/WebAuthn" parent="shibboleth.AuthenticationFlow"
p:order="%{idp.authn.webauthn.order:1000}"
p:nonBrowserSupported="%{idp.authn.webauthn.nonBrowserSupported:false}"
@@ -40,6 +64,24 @@
c:_0="#{'%{idp.authn.webauthn.supportedPrincipals:}'.trim()}" />
</property>
</bean>
+
+
+ <!-- Admim flow for WebAuthn credential registration -->
+ <bean parent="shibboleth.AdminFlow"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnCredentialRegistrationFlowDescriptor"
+ p:loggingId="%{idp.authn.webauthn.admin.registration.logging:WebAuthnCredentialRegistration}"
+ p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByIPAddress}"
+ p:nonBrowserSupported="%{idp.authn.webauthn.admin.registration.nonBrowserSupported:false}"
+ p:authenticated="true">
+ <property name="postAuthenticationFlows">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.authn.webauthn.admin.registration.postAuthenticationFlows:}'.trim()}" />
+ </property>
+ <property name="defaultAuthenticationMethodsByString">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.authn.webauthn.admin.registration.defaultAuthenticationMethods:}'.trim()}" />
+ </property>
+ </bean>
</beans>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
new file mode 100644
index 0000000..303fa5a
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
@@ -0,0 +1,43 @@
+<?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">
+
+ <bean id="shibboleth.AdminProfileId" class="java.lang.String"
+ c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
+
+ <bean id="PopulateWebauthnAuthenticationContext" scope="prototype"
+ parent="AbstractPopulateWebauthnAuthenticationContext"
+ p:usernameRequiredPredicate="true">
+ <property name="usernameLookupStrategy">
+ <bean id="UsernameFromAuthenticationContextLookupStrategy"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy"/>
+ </property>
+ </bean>
+
+ <bean id="ExtractPublicKeyCredentialFromFormRequest" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+
+
+ <bean id="ValidatePublicKeyCredential" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidatePublicKeyCredential" />
+
+
+ <bean id="StorePublicKeyCredential" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.StorePublicKeyCredential"
+ p:storageService-ref="shibboleth.authn.webauthn.StorageService"/>
+
+ <!-- postconfig.xml? -->
+ <bean id="shibboleth.authn.webauthn.StorageService" lazy-init="true"
+ class="org.opensaml.storage.impl.MemoryStorageService"
+ p:cleanupInterval="PT10M" />
+
+</beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
new file mode 100644
index 0000000..0ac016a
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -0,0 +1,78 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="admin.abstract">
+
+
+ <!-- Start action. -->
+
+ <action-state id="InitializeProfileRequestContext">
+ <evaluate expression="InitializeProfileRequestContext" />
+ <evaluate expression="FlowStartPopulateAuditContext" />
+ <evaluate expression="'proceed'" />
+
+ <!-- Branch to determine if authentication is required. -->
+ <transition on="proceed" to="DoAdminPreamble" />
+ </action-state>
+
+ <!-- Resume actual flow processing. -->
+
+ <action-state id="DoProfileWork">
+ <evaluate expression="CheckAccess" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="PopulateWebAuthnContext" />
+ </action-state>
+
+ <action-state id="PopulateWebAuthnContext">
+ <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+ <evaluate expression="GenerateServerChallenge"/>
+ <evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="DisplayWebAuthnView" />
+ </action-state>
+
+ <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-register">
+ <on-render>
+ <evaluate expression="environment" result="viewScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="viewScope.authenticationContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext))" result="viewScope.webauthnContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="viewScope.rpUIContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="viewScope.authenticationErrorContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="viewScope.authenticationWarningContext" />
+ <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+ </on-render>
+ <transition on="proceed" to="ExtractPublicKeyCredential" />
+
+ </view-state>
+
+ <action-state id="ExtractPublicKeyCredential">
+ <evaluate expression="ExtractPublicKeyCredentialFromFormRequest"/>
+ <evaluate expression="ValidatePublicKeyCredential"/>
+ <evaluate expression="StorePublicKeyCredential"/>
+
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="DisplayWebAuthnSuccessfulRegistration" />
+ </action-state>
+
+ <end-state id="DisplayWebAuthnSuccessfulRegistration" view="webauthn/webauthn-registered">
+ <on-entry>
+ <evaluate expression="environment" result="requestScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="requestScope.authenticationContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext))" result="requestScope.webauthnContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="requestScope.rpUIContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="requestScope.authenticationErrorContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="requestScope.authenticationWarningContext" />
+ <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="requestScope.encoder" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="requestScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="requestScope.response" />
+ </on-entry>
+ </end-state>
+
+ <bean-import resource="webauthn-registration-beans.xml" />
+ <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/authn/webauthn/webauthn-abstract-beans.xml" />
+
+</flow>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
new file mode 100644
index 0000000..8dce35c
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
@@ -0,0 +1,54 @@
+<?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">
+
+ <bean id="AbstractPopulateWebauthnAuthenticationContext" scope="prototype" abstract="true"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebauthnAuthenticationContext" />
+
+ <bean id="GenerateServerChallenge" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge" />
+
+ <bean id="CreatePublicKeyCredentialRequestOptions" scope="prototype"
+ class=" net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"
+ p:webauthnClient-ref="shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory" />
+
+ <!-- Singleton clients -->
+ <bean id="shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory" scope="singleton"
+ class="net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory"
+ p:relyingPartyId="localhost" p:relyingPartyName="Shibboleth"
+ p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+
+ <!-- Create a default object mapper. Setup should not change once injected -->
+ <bean id="shibboleth.authn.WebAuthn.JSONObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
+
+ <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+ p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="setSerializationInclusion">
+ <property name="arguments">
+ <util:constant static-field="com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL" />
+ </property>
+ </bean>
+
+ <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+ p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="registerModule">
+ <property name="arguments">
+ <bean class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
+ </property>
+ </bean>
+
+
+
+ <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+ p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="setDateFormat">
+ <property name="arguments">
+ <bean class="java.text.SimpleDateFormat" c:_0="yyyy-MM-dd'T'HH:mm:ss.SSSZZ" />
+ </property>
+ </bean>
+
+</beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 3dd79d0..283c197 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -1,15 +1,25 @@
<?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
+ 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">
-
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <bean id="PopulateWebauthnAuthenticationContext" scope="prototype"
+ parent="AbstractPopulateWebauthnAuthenticationContext"
+ p:usernameRequiredPredicate="false">
+ </bean>
+
+ <bean id="ValidateWebAuthnAssertion" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion" />
+
+ <bean id="ExtractPublicKeyAssertionFromFormRequest" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyAssertionFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+
</beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
index d8cec3f..5942fec 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
@@ -1,5 +1,41 @@
<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd">
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="authn.abstract, authn/conditions">
+ <action-state id="PopulateWebauthnContext">
+ <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+ <evaluate expression="GenerateServerChallenge"/>
+ <evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="DisplayWebAuthnView" />
+ </action-state>
+
+
+ <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-authn">
+ <on-render>
+ <evaluate expression="environment" result="viewScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="viewScope.authenticationContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext))" result="viewScope.webauthnContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="viewScope.rpUIContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="viewScope.authenticationErrorContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="viewScope.authenticationWarningContext" />
+ <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+ </on-render>
+ <transition on="proceed" to="AuthenticatePublicKeyCredential" />
+
+ </view-state>
+
+ <action-state id="AuthenticatePublicKeyCredential">
+ <evaluate expression="ExtractPublicKeyAssertionFromFormRequest"/>
+ <evaluate expression="ValidateWebAuthnAssertion"/>
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="proceed" />
+ </action-state>
+
+ <bean-import resource="webauthn-beans.xml" />
+ <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/authn/webauthn/webauthn-abstract-beans.xml" />
</flow>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthnFlowChooser/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthnFlowChooser/webauthn-flow.xml
new file mode 100644
index 0000000..7e8dab1
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthnFlowChooser/webauthn-flow.xml
@@ -0,0 +1,18 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="authn.abstract, authn/conditions">
+
+ <!-- end-state transitions are inherited from authn-events-flow.xml -->
+ <view-state id="DisplayWebAuthnChoiceView" view="webauthn/webauthn-selector">
+ <on-render>
+ <evaluate expression="environment" result="viewScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+ </on-render>
+ <transition on="proceed" to="WebAuthnFlow" />
+ <transition on="password" to="PasswordFlow" />
+ </view-state>
+
+</flow>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js
new file mode 100644
index 0000000..00c1974
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js
@@ -0,0 +1,163 @@
+
+
+const base64abc = [
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
+];
+
+
+const base64codes = [
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255,
+ 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
+ 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
+];
+
+function bytesToBase64(bytes) {
+ let result = '', i, l = bytes.length;
+ for (i = 2; i < l; i += 3) {
+ result += base64abc[bytes[i - 2] >> 2];
+ result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+ result += base64abc[((bytes[i - 1] & 0x0F) << 2) | (bytes[i] >> 6)];
+ result += base64abc[bytes[i] & 0x3F];
+ }
+ if (i === l + 1) { // 1 octet yet to write
+ result += base64abc[bytes[i - 2] >> 2];
+ result += base64abc[(bytes[i - 2] & 0x03) << 4];
+ result += "==";
+ }
+ if (i === l) { // 2 octets yet to write
+ result += base64abc[bytes[i - 2] >> 2];
+ result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+ result += base64abc[(bytes[i - 1] & 0x0F) << 2];
+ result += "=";
+ }
+ return result;
+}
+
+function responseToObject(response) {
+ if (response.u2fResponse) {
+ return response;
+ } else {
+ let clientExtensionResults = {};
+
+ try {
+ clientExtensionResults = response.getClientExtensionResults();
+ } catch (e) {
+ console.error('getClientExtensionResults failed', e);
+ }
+
+ if (response.response.attestationObject) {
+ return {
+ type: response.type,
+ id: response.id,
+ response: {
+ attestationObject: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.attestationObject))),
+ clientDataJSON: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.clientDataJSON))),
+ },
+ clientExtensionResults,
+ };
+ } else {
+ return {
+ type: response.type,
+ id: response.id,
+ response: {
+ authenticatorData: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.authenticatorData))),
+ clientDataJSON: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.clientDataJSON))),
+ signature: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.signature))),
+ userHandle: response.response.userHandle && Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.userHandle))),
+ },
+ clientExtensionResults,
+ };
+ }
+ }
+}
+
+if (!window.PublicKeyCredential) {
+ console.log("Bad client, died");
+}
+
+function ensureUint8Array(arg) {
+ if (arg instanceof ArrayBuffer) {
+ return new Uint8Array(arg);
+ } else {
+ return arg;
+ }
+}
+/**
+ * use this to make a Base64 encoded string URL friendly,
+ * i.e. '+' and '/' are replaced with '-' and '_' also any trailing '='
+ * characters are removed
+ *
+ * @param {String} str the encoded string
+ * @returns {String} the URL friendly encoded String
+ */
+function Base64EncodeUrl(str) {
+ return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
+}
+
+const publicKeyCredentialCreationOptions = {
+ challenge: Uint8Array.from(
+ "$webauthnContext.serverChallengeBase64", c => c.charCodeAt(0)),
+ rp: {
+ name: "Shib",
+ id: "localhost",
+ },
+ user: {
+ id: Uint8Array.from(
+ "UZSL85T9AFC", c => c.charCodeAt(0)),
+ name: "phil1001 at jisc.ac.uk",
+ displayName: "Phil",
+ },
+ pubKeyCredParams: [{ alg: -7, type: "public-key" }],
+ authenticatorSelection: {
+ authenticatorAttachment: "cross-platform",
+ userVerification: "preferred"
+ },
+ timeout: 60000,
+ attestation: "direct"
+};
+
+async function authenticate() {
+ try{
+ var challenge = "$webauthnContext.serverChallengeBase64";
+ console.log("Challenge: "+challenge);
+ const assertion = await navigator.credentials.get({
+ publicKey: {
+ challenge: Uint8Array.from(
+ challenge, c => c.charCodeAt(0)),
+ timeout: 60000,
+ }
+ });
+ console.log(assertion);
+ document.getElementById("publicKeyAssertion").value = JSON.stringify(responseToObject(assertion));
+ document.getElementById("authn-submit").click();
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+
+async function register() {
+ const credential = await navigator.credentials.create({
+ publicKey: publicKeyCredentialCreationOptions
+ }).catch(console.error);
+
+ console.log("Credential: " + credential);
+ console.log(responseToObject(credential));
+ console.log(JSON.stringify(credential.response));
+ try {
+ document.getElementById("publicKeyCredential").value = JSON.stringify(responseToObject(credential));
+
+ } catch (err) {
+ console.log(err);
+ }
+
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-yubico.js b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-yubico.js
new file mode 100644
index 0000000..2cf5335
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-yubico.js
@@ -0,0 +1,171 @@
+// Copyright (c) 2018, Yubico AB
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+(function(root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define(['base64url'], factory);
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('base64url'));
+ } else {
+ root.webauthn = factory(root.base64url);
+ }
+})(this, function(base64url) {
+
+ function extend(obj, more) {
+ return Object.assign({}, obj, more);
+ }
+
+ /**
+ * Create a WebAuthn credential.
+ *
+ * @param request: object - A PublicKeyCredentialCreationOptions object, except
+ * where binary values are base64url encoded strings instead of byte arrays
+ *
+ * @return a PublicKeyCredentialCreationOptions suitable for passing as the
+ * `publicKey` parameter to `navigator.credentials.create()`
+ */
+ function decodePublicKeyCredentialCreationOptions(request) {
+ const excludeCredentials = request.excludeCredentials.map(credential => extend(
+ credential, {
+ id: base64url.toByteArray(credential.id),
+ }));
+
+ const publicKeyCredentialCreationOptions = extend(
+ request, {
+ attestation: 'direct',
+ user: extend(
+ request.user, {
+ id: base64url.toByteArray(request.user.id),
+ }),
+ challenge: base64url.toByteArray(request.challenge),
+ excludeCredentials,
+ });
+
+ return publicKeyCredentialCreationOptions;
+ }
+
+ /**
+ * Create a WebAuthn credential.
+ *
+ * @param request: object - A PublicKeyCredentialCreationOptions object, except
+ * where binary values are base64url encoded strings instead of byte arrays
+ *
+ * @return the Promise returned by `navigator.credentials.create`
+ */
+ function createCredential(request) {
+ return navigator.credentials.create({
+ publicKey: decodePublicKeyCredentialCreationOptions(request),
+ });
+ }
+
+ /**
+ * Perform a WebAuthn assertion.
+ *
+ * @param request: object - A PublicKeyCredentialRequestOptions object,
+ * except where binary values are base64url encoded strings instead of byte
+ * arrays
+ *
+ * @return a PublicKeyCredentialRequestOptions suitable for passing as the
+ * `publicKey` parameter to `navigator.credentials.get()`
+ */
+ function decodePublicKeyCredentialRequestOptions(request) {
+ const allowCredentials = request.allowCredentials && request.allowCredentials.map(credential => extend(
+ credential, {
+ id: base64url.toByteArray(credential.id),
+ }));
+
+ const publicKeyCredentialRequestOptions = extend(
+ request, {
+ allowCredentials,
+ challenge: base64url.toByteArray(request.challenge),
+ });
+
+ return publicKeyCredentialRequestOptions;
+ }
+
+ /**
+ * Perform a WebAuthn assertion.
+ *
+ * @param request: object - A PublicKeyCredentialRequestOptions object,
+ * except where binary values are base64url encoded strings instead of byte
+ * arrays
+ *
+ * @return the Promise returned by `navigator.credentials.get`
+ */
+ function getAssertion(request) {
+ console.log('Get assertion', request);
+ return navigator.credentials.get({
+ publicKey: decodePublicKeyCredentialRequestOptions(request),
+ });
+ }
+
+
+ /** Turn a PublicKeyCredential object into a plain object with base64url encoded binary values */
+ function responseToObject(response) {
+ if (response.u2fResponse) {
+ return response;
+ } else {
+ let clientExtensionResults = {};
+
+ try {
+ clientExtensionResults = response.getClientExtensionResults();
+ } catch (e) {
+ console.error('getClientExtensionResults failed', e);
+ }
+
+ if (response.response.attestationObject) {
+ return {
+ type: response.type,
+ id: response.id,
+ response: {
+ attestationObject: bytesToBase64(response.response.attestationObject),
+ clientDataJSON: bytesToBase64(response.response.clientDataJSON),
+ },
+ clientExtensionResults,
+ };
+ } else {
+ return {
+ type: response.type,
+ id: response.id,
+ response: {
+ authenticatorData: base64url.fromByteArray(response.response.authenticatorData),
+ clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON),
+ signature: base64url.fromByteArray(response.response.signature),
+ userHandle: response.response.userHandle && base64url.fromByteArray(response.response.userHandle),
+ },
+ clientExtensionResults,
+ };
+ }
+ }
+ }
+
+ return {
+ decodePublicKeyCredentialCreationOptions,
+ decodePublicKeyCredentialRequestOptions,
+ createCredential,
+ getAssertion,
+ responseToObject,
+ };
+
+});
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
new file mode 100644
index 0000000..1e98647
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -0,0 +1,72 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText(" idp.css", "/css/placeholder.css" )">
+ <script src="$request.getContextPath()/js/webauthn-shib.js"></script>
+</head>
+
+<body>
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText(" idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+
+ #set ($serviceName = $rpUIContext.serviceName)
+ #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+ <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+ #end
+ </header>
+
+ <section>
+
+ <div class="content">
+ <div class="column one">
+ <button class="form-element form-button" onclick="authenticate();">Authenticate</button>
+ <form id="authn-form" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <textarea id="publicKeyAssertion" name="publicKeyAssertion" rows="10" cols="50">
+ </textarea>
+ <button id="authn-submit" type="submit" name="_eventId_proceed">Submit Authentication</button>
+ </form>
+
+
+ </div>
+
+ </div>
+ </section>
+
+ <footer>
+ <div class="container container-footer">
+ <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </div>
+
+</body>
+
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
new file mode 100644
index 0000000..ab1baf0
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -0,0 +1,73 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+ <script src="$request.getContextPath()/js/webauthn-shib.js"></script>
+</head>
+
+
+
+<body>
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText(" idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+ #set ($serviceName = $rpUIContext.serviceName)
+ #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+ <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+ #end
+ </header>
+
+ <section>
+
+ <div class="content">
+ <div class="column one">
+ <button class="form-element form-button" onclick="register();">Register</button>
+
+ <form action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <textarea id="publicKeyCredential" name="publicKeyCredential" rows="10" cols="50"></textarea>
+ <button id="reg-submit" type="submit" name="_eventId_proceed">Submit Registration</button>
+ </form>
+
+
+ </div>
+
+ </div>
+ </section>
+ </main>
+ <footer>
+ <div class="container container-footer">
+ <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </div>
+
+</body>
+
+</html>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registered.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registered.vm
new file mode 100644
index 0000000..2ed681d
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registered.vm
@@ -0,0 +1,57 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+ </head>
+
+
+
+ <body>
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+
+ #set ($serviceName = $rpUIContext.serviceName)
+ #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+ <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+ #end
+ </header>
+ <section>
+ You have registered your authenticator credentials succesfully.
+ </section>
+ </main>
+
+ <footer>
+ <div class="container container-footer">
+ <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </div>
+
+ </body>
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
new file mode 100644
index 0000000..cf78851
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
@@ -0,0 +1,66 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+ </head>
+ <body>
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+
+ #set ($serviceName = $rpUIContext.serviceName)
+ #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+ <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+ #end
+ </header>
+
+ <section>
+
+ <form action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <div class="grid">
+ <div class="grid-item">
+ <button type="submit" name="_eventId_proceed">Use Passkey</button>
+ </div>
+ </div>
+ </form>
+ <form action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <div class="grid">
+ <div class="grid-item">
+ <button type="submit" name="_eventId_password">Use Password</button>
+ </div>
+ </div>
+ </form>
+
+ </section>
+ </main>
+ <footer class="footer">
+ <div class="cc">
+ <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </body>
+
+ </body>
+</html>
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list