[java-idp-plugin-webauthn] branch main updated: Further basic registration and authentication flow
Phil Smart
philip.smart at jisc.ac.uk
Fri Dec 1 14:46:35 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=b1eb3e5ffd362af78cd39adf8bda48702b7972ac
The following commit(s) were added to refs/heads/main by this push:
new b1eb3e5 Further basic registration and authentication flow
b1eb3e5 is described below
commit b1eb3e5ffd362af78cd39adf8bda48702b7972ac
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Dec 1 14:46:33 2023 +0000
Further basic registration and authentication flow
- Add validation to assertion and attestation responses
- Move to Yubico types for webauthn interface
- Improve actions
---
.../AbstractWebAuthnAuthenticationAction.java | 8 +-
...ava => AbstractWebAuthnRegistrationAction.java} | 91 ++++-----
.../authn/webauthn/AssertionFailureException.java | 65 +++++++
.../plugin/authn/webauthn/CredentialPublicKey.java | 98 ++++++++++
.../webauthn/RegistrationFailureException.java | 65 +++++++
.../webauthn/WebAuthnAuthenticationClient.java | 57 +++---
.../webauthn/context/BaseWebAuthnContext.java | 88 +++++++++
.../context/WebAuthnAuthenticationContext.java | 177 +++++------------
.../context/WebAuthnRegistrationContext.java | 142 ++++++++++++++
.../CreatePublicKeyCredentialCreationOptions.java | 74 +++++---
...ctAuthenticatorAttestationFromFormRequest.java} | 20 +-
.../impl/GenerateUserHandle.java} | 31 +--
.../impl/PopulateWebAuthnRegistrationContext.java} | 68 +++----
.../admin/impl/StorePublicKeyCredential.java | 210 +++++++++++++++++++++
.../ValidateAuthenticatorAttestationResponse.java | 55 +++---
.../impl/YubicoWebauthnAuthenticationClient.java | 158 +++++++---------
.../client/impl/YubicoWebauthnClientFactory.java | 53 ++++--
.../CreatePublicKeyCredentialRequestOptions.java | 41 +++-
...ractAuthenticatorAssertionFromFormRequest.java} | 24 ++-
.../webauthn/impl/GenerateServerChallenge.java | 56 +++++-
... => PopulateWebAuthnAuthenticationContext.java} | 6 +-
.../webauthn/impl/ValidateWebAuthnAssertion.java | 34 ++--
.../storage/impl/CredentialPublicKeyHolder.java | 91 +++++++++
.../storage/impl/CredentialRegistration.java | 1 +
.../storage/impl/StorePublicKeyCredential.java | 161 ----------------
.../META-INF/net.shibboleth.idp/postconfig.xml | 13 +-
.../webauthn-registration-beans.xml | 59 +++---
.../webauthn-registration-flow.xml | 9 +-
.../authn/WebAuthn/webauthn-abstract-beans.xml | 10 +-
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 19 +-
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 4 +-
.../YubicoWebauthnAuthenticationClientTest.java | 64 +++----
.../authn/webauthn/impl/AbstractWebAuthnTest.java | 7 +
.../impl/ValidatePublicKeyCredentialTest.java | 8 +-
34 files changed, 1348 insertions(+), 719 deletions(-)
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
index a9448cc..d81e6ee 100644
--- 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
@@ -58,7 +58,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
/** Lookup strategy to locate the webauthn authentication context. */
@Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
- /** The Duo authentication Context.*/
+ /** The WebAuthn authentication Context.*/
@NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
/** The WebAuthn client to use.*/
@@ -95,7 +95,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
/**
- * Set Duo authentication context lookup strategy to use.
+ * Set WebAuthn authentication context lookup strategy to use.
*
* @param strategy lookup strategy
*/
@@ -104,7 +104,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
checkSetterPreconditions();
webauthnContextLookupStrategy =
- Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+ Constraint.isNotNull(strategy, "WebAuthnContextLookuplookup strategy cannot be null");
}
/** {@inheritDoc} */
@@ -128,7 +128,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
webauthnContext = webauthnContextLookupStrategy.apply(profileRequestContext);
if (webauthnContext == null) {
- log.warn("{} No Webauthn context returned by lookup strategy",getLogPrefix());
+ log.warn("{} No WebAuthn authentication context returned by lookup strategy",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return false;
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/AbstractWebAuthnRegistrationAction.java
similarity index 55%
copy from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
copy to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
index a9448cc..017d9e8 100644
--- 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/AbstractWebAuthnRegistrationAction.java
@@ -23,13 +23,13 @@ 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.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -37,29 +37,29 @@ import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * <p>A base class for webauthn authentication related actions.</p>
+ * <p>A base class for WebAuthn registration related admin 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>In addition to the work performed by {@link AbstractProfileAction}, this action also looks up
+ * and makes available the {@link WebAuthnRegistrationContext}.</p>
*
- * <p>Webauthn authentication action implementations should override the
- * {@link #doExecute(ProfileRequestContext, AuthenticationContext, WebAuthnAuthenticationContext)}
- * method.</p>
+ * <p>WebAuthn registration action implementations should override the
+ * {@link #doExecute(ProfileRequestContext, WebAuthnRegistrationContext)} method.</p>
*
- * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
- * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- * @post <pre>AuthenticationContext.getSubcontext(WebAuthnAuthenticationContext.class) != null</pre>
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(ProfileRequestContext.class) != null</pre>
+ * @post <pre>AuthenticationContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
*/
-public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthenticationAction {
+public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfileAction {
/** Class logger. */
- @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnAuthenticationAction.class);
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnRegistrationAction.class);
- /** Lookup strategy to locate the webauthn authentication context. */
- @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
+ /** Lookup strategy to locate the webauthn registration context. */
+ @Nonnull
+ private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
- /** The Duo authentication Context.*/
- @NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
+ /** The WebAuthn registration Context.*/
+ @NonnullBeforeExec private WebAuthnRegistrationContext webauthnRegistrationContext;
/** The WebAuthn client to use.*/
@NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
@@ -87,23 +87,22 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
/** Constructor.*/
- protected AbstractWebAuthnAuthenticationAction() {
- //prc -> ac -> dc
- webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
- compose(new ChildContextLookup<>(AuthenticationContext.class));
+ protected AbstractWebAuthnRegistrationAction() {
+ //prc -> WebAuthnContext
+ webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
}
/**
- * Set Duo authentication context lookup strategy to use.
+ * Set WebAuthn registration context lookup strategy to use.
*
* @param strategy lookup strategy
*/
- public void setWebauthnContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+ public void setWebauthnRegistrationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
checkSetterPreconditions();
- webauthnContextLookupStrategy =
+ webauthnRegistrationContextLookupStrategy =
Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
}
@@ -119,50 +118,44 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
/** {@inheritDoc} */
@Override
- protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext) {
+ protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ if (!super.doPreExecute(profileRequestContext)) {
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);
+ webauthnRegistrationContext = webauthnRegistrationContextLookupStrategy.apply(profileRequestContext);
+ if (webauthnRegistrationContext == null) {
+ log.warn("{} No WebAuthn registration context returned by lookup strategy",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
- return doPreExecute(profileRequestContext, authenticationContext, webauthnContext);
+ return doPreExecute(profileRequestContext, webauthnRegistrationContext);
}
/**
- * Delegates to {@link #doExecute(ProfileRequestContext, AuthenticationContext,
- * WebAuthnAuthenticationContext)} to perform the actual authentication. Implementations can not
- * override this method.
+ * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnAuthenticationContext)} to perform the
+ * actual action. 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) {
+ protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- doExecute(profileRequestContext,authenticationContext, webauthnContext);
+ doExecute(profileRequestContext, webauthnRegistrationContext);
}
/**
- * Performs this authentication action's pre-execute step. Default implementation just returns true.
+ * Performs this admin 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
+ * @param context the WebAuthn registration context
*
* @return true iff execution should continue
*/
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext context) {
return true;
}
@@ -171,12 +164,10 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
* should override this method.
*
* @param profileRequestContext the current IdP profile request context
- * @param authenticationContext the current authentication context
- * @param context the webauthn authentication context
+ * @param context the WebAuthn registration context
*/
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext context) {
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AssertionFailureException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AssertionFailureException.java
new file mode 100644
index 0000000..a678eaa
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AssertionFailureException.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ * Exception that is throw if an assertion is not valid.
+ */
+public class AssertionFailureException extends WebAuthnAuthenticationClientException {
+
+ /** Generated serial UID.*/
+ private static final long serialVersionUID = 1266841324008931444L;
+
+ /**
+ * Constructor.
+ *
+ */
+ public AssertionFailureException() {
+ super();
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param cause exception to be wrapped by this one
+ */
+ public AssertionFailureException(final String message, final Throwable cause) {
+ super(message, cause);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public AssertionFailureException(final String message) {
+ super(message);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause exception to be wrapped by this one
+ */
+ public AssertionFailureException(final Throwable cause) {
+ super(cause);
+
+ }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/CredentialPublicKey.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/CredentialPublicKey.java
new file mode 100644
index 0000000..db67b76
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/CredentialPublicKey.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * The public key portion of the asymmetric credential key pair created by the authenticator during registration.
+ *
+ * <p>This is stored and later used to validate assertions generated in authentication ceremonies.</p>
+ */
+public class CredentialPublicKey {
+
+ /**
+ * Identifies the public key credential source and its authentication assertions. Generated by the
+ * authenticator and is an opaque random byte array.
+ */
+ @Nonnull private final byte[] credentialId;
+
+ /** The user ID as specified by the IdP that maps a credential public key to a user.*/
+ @Nonnull private final byte[] userHandle;
+
+ /** The COSE_Key encoded credential public key used to verify authentication assertions.*/
+ @Nonnull private final byte[] publicKeyCose;
+
+ /** The current signature count for this credential from the authenticator.*/
+ private final long signatureCount;
+
+ /**
+ * Constructor.
+ *
+ * @param credentialId the identifier of the credential.
+ * @param userHandle the user ID that maps this credential public key to a user.
+ * @param publicKeyCose the public key in COSE_Key format
+ * @param signatureCount the signature counter for this credential.
+ */
+ public CredentialPublicKey(@Nonnull final byte[] credId, @Nonnull final byte[] handle,
+ @Nonnull final byte[] keyCose, final long signCount) {
+ super();
+ credentialId = Constraint.isNotNull(credId, "CredentialID can not be null");
+ userHandle = Constraint.isNotNull(handle, "UserHandle can not be null");
+ publicKeyCose = Constraint.isNotNull(keyCose, "PublicKey in COSE_Key format can not be null");
+ signatureCount = signCount;
+ }
+
+ /**
+ * Get the credential's identifier.
+ *
+ * @return the credentialId.
+ */
+ @Nonnull public byte[] getCredentialId() {
+ return credentialId;
+ }
+
+ /**
+ * Get the user handle of this identifier.
+ *
+ * @return the userHandle.
+ */
+ @Nonnull public byte[] getUserHandle() {
+ return userHandle;
+ }
+
+ /**
+ * Get the public key in COSE_Key format.
+ *
+ * @return the publicKeyCose.
+ */
+ @Nonnull public byte[] getPublicKeyCose() {
+ return publicKeyCose;
+ }
+
+ /**
+ * Get the signature count from the authenticator.
+ *
+ * @return the signatureCount.
+ */
+ public long getSignatureCount() {
+ return signatureCount;
+ }
+
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/RegistrationFailureException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/RegistrationFailureException.java
new file mode 100644
index 0000000..ec6731a
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/RegistrationFailureException.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ * Exception that is throw if a public key credential registration is not valid.
+ */
+public class RegistrationFailureException extends WebAuthnAuthenticationClientException {
+
+ /** Generated serial UID.*/
+ private static final long serialVersionUID = 1266841324008931444L;
+
+ /**
+ * Constructor.
+ *
+ */
+ public RegistrationFailureException() {
+ super();
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param cause exception to be wrapped by this one
+ */
+ public RegistrationFailureException(final String message, final Throwable cause) {
+ super(message, cause);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public RegistrationFailureException(final String message) {
+ super(message);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause exception to be wrapped by this one
+ */
+ public RegistrationFailureException(final Throwable cause) {
+ super(cause);
+
+ }
+
+}
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
index bbcbc5d..53b820e 100644
--- 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
@@ -4,18 +4,25 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
/**
* A client that manages the entire webauthn authentication and registration ceremony.
*
+ * <p>Tied to the Yubico library data model, other clients will need to be adapted to support this model, and provide
+ * translations between their internal representation and the types required.</p>
+ *
* <p>Implementations must be thread-safe</p>
*/
-//TODO integrate this with the CredentialValidator interface?
@ThreadSafe
public interface WebAuthnAuthenticationClient {
/**
- * Create a JSON serialized PublicKeyCredentialRequestOptions.
+ * Create a PublicKeyCredentialRequestOptions for the WebAuthn 'get' call to generate an authentication assertion.
*
* @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
@@ -23,61 +30,65 @@ public interface WebAuthnAuthenticationClient {
* @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
* @param the challenge used when creating new credentials
*
- * @return a JSON serialized PublicKeyCredentialRequestOptions object.
+ * @return a PublicKeyCredentialRequestOptions object
*
* @throws WebAuthnAuthenticationClientException if there is an error generating the authentication request
*
*/
- //TODO how do we guarantee this is JSON, or just return the Yubico object?
- @Nonnull String createAuthenticationRequest(@Nullable final String username, @Nullable final byte[] userHandle,
- @Nonnull final byte[] challenge) throws WebAuthnAuthenticationClientException;
+ @Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username,
+ @Nullable final byte[] userHandle, @Nonnull final byte[] challenge)
+ throws WebAuthnAuthenticationClientException;
/**
- * Create a JSON serialized PublicKeyCredentialCreationOptions.
+ * Create a PublicKeyCredentialCreationOptions for the WebAuthn 'create' call to generate a registration
+ * attestation.
*
* @param username the username of the username that has been pre-identified. Can be {@code null}
* if no user has been identified, and the IdP is requesting the client discover
* the credential.
* @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
- * @param the challenge used when creating new credentials
+ * @param the challenge to sign when creating new credentials
*
- * @return a JSON serialized PublicKeyCredentialCreationOptions object. Can be {@code null} if
- * one could not be created.
+ * @return a PublicKeyCredentialCreationOptions object to supply the WebAuthn 'create' call.
*
* @throws WebAuthnAuthenticationClientException if there is an error generating the creation request
*/
- //TODO how do we guarantee this is JSON, or just return the Yubico object?
- @Nullable String createRegistrationRequest(@Nullable final String username, @Nullable final byte[] userHandle,
- @Nonnull final byte[] challenge) throws WebAuthnAuthenticationClientException;
+ @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(@Nullable final String username,
+ @Nullable final byte[] userHandle, @Nonnull final byte[] challenge)
+ throws WebAuthnAuthenticationClientException;
/**
- * Validate the Authenticator Assertion Response.
+ * Validate the Authenticator Assertion Response from an authentication request
*
* @param usernmae ....TODO
* @param userHandle ....TODO
* @param publicKeyCredentialRequestOptions the options used when generating an assertion for authentication.
* @param authenticatorAssertionResponse the JSON representation of the assertion response.
*
- * @return true if the assertion was verified successfully, false otherwise.
+ * @return an assertion result if the assertion was valid.
*
- * @throws WebAuthnAuthenticationClientException if there is an error during validation
+ * @throws WebAuthnAuthenticationClientException if the assertion is not valid
*/
- boolean validateAuthenticatorAssertionResponse(@Nullable final String username,
- @Nullable final byte[] userHandle, @Nonnull final String publicKeyCredentialRequestOptions,
+ AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username,
+ @Nullable final byte[] userHandle,
+ @Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
@Nonnull final String authenticatorAssertionResponse)
- throws WebAuthnAuthenticationClientException;
+ throws AssertionFailureException;
/**
- * Validate a registration request.
+ * Validate the Authenticator Attestation Response from a registration request.
*
* @param publicKeyCredentialCreationOptions the options used when requesting a new public key credential
* @param authenticatorAttestationResponse the response to the client's request to create a public key credential
*
- * @return true if the registration is valid, false otherwise.
+ * @return a registration result of the registration was valid
+ *
+ * @throws RegistrationFailureException if the registration is not valid
*/
- boolean validateRegistration(@Nonnull final String publicKeyCredentialCreationOptions,
- @Nonnull final String authenticatorAttestationResponse);
+ RegistrationResult validateAuthenticatorAttestationResponse(
+ @Nonnull final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions,
+ @Nonnull final String authenticatorAttestationResponse) throws RegistrationFailureException;
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
new file mode 100644
index 0000000..4cf3a25
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
@@ -0,0 +1,88 @@
+/*
+ * 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.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * The base WebAuthn context for both registration and authentication ceremonies.
+ */
+public class BaseWebAuthnContext extends BaseContext {
+
+ /** The challenge sent to the authenticator in both registration and authentication ceremonies.*/
+ @Nullable private byte[] serverChallenge;
+
+ /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+ @Nullable private byte[] userHandle;
+
+ /**
+ * Get the server challenge sent to the authenticator.
+ *
+ * @return the server challenge.
+ */
+ @Nullable public byte[] getServerChallenge() {
+ return serverChallenge;
+ }
+
+ //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);
+ }
+
+ /**
+ * Set the server challenge which forms part of the information 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 BaseWebAuthnContext 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;
+ }
+
+
+ /**
+ * Set the user handle used to map public key credentials to user accounts. Maximum 64 bytes
+ *
+ * @param handle The userHandle to set.
+ */
+ public BaseWebAuthnContext setUserHandle(@Nonnull final byte[] handle) {
+ Constraint.isNotEmpty(handle,"UserHandle can not be null or empty");
+ Constraint.isLessThan(65, handle.length, "UserHandle must be maximum 64 bytes");
+ userHandle = handle;
+ return this;
+ }
+
+ /**
+ * Get the userId used to map public key credentials to user accounts.
+ *
+ * @return the userHandle.
+ */
+ public byte[] getUserHandle() {
+ return userHandle;
+ }
+
+}
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
index 83a9f9b..7729c59 100644
--- 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
@@ -4,18 +4,14 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
-import org.opensaml.messaging.context.BaseContext;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.logic.Constraint;
-//TODO if we use a client which supports more than just Yubico (not too realistic) then we need to generalise these types
+/** Authentication context for processing WebAuthn Authentication Ceremonies. */
@NotThreadSafe
-public final class WebAuthnAuthenticationContext extends BaseContext {
-
- @Nullable private byte[] serverChallenge;
+public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
/** The credential public key encoded in COSE_Key format.*/
@Nullable private byte[] publicKey;
@@ -26,56 +22,19 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
/** 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 String authenticatorAssertionResponse;
-
- /** An assertion response that is the result of an authentication.*/
- @Nullable private String authenticatorAttestationResponse;
-
- /** The public key credential creation options for registration.*/
- @Nullable private String publicKeyCredentialCreationOptions;
/** The public key credential request options for authentication.*/
- @Nullable private String publicKeyCredentialRequestOptions;
+ @Nullable private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
- /**
- * 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 attestation response as a result of creating a new credential.
- *
- * @return Returns the authenticator attestation response.
- */
- @Nullable public String getAuthenticatorAttestationResponse() {
- return authenticatorAttestationResponse;
- }
-
- /**
- * Set the attestation response as a result of creating a new credential.
- *
- * @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
- */
- public void setAuthenticatorAttestationResponse(@Nullable final String attestation) {
- authenticatorAttestationResponse = attestation;
- }
+ // TODO maybe generate the JSON during the webflow action to populate the view
+ /** The public key credential request options for authentication represented as a JSON string.*/
+ @Nullable private String publicKeyCredentialRequestOptionsJSON;
+ /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+ @Nullable private byte[] userHandle;
+
/**
* Gets the username.
*
@@ -98,23 +57,6 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
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.
@@ -125,45 +67,7 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
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.
*
@@ -182,39 +86,17 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
credentialId = Constraint.isNotNull(id, "Credential ID can not be null");
return this;
}
-
- //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);
- }
- /**
- * Set the options used to create public key credentials.
- *
- * @param options The publicKeyCredentialCreationOptions to set.
- */
- public void setPublicKeyCredentialCreationOptions(@Nullable final String options) {
- publicKeyCredentialCreationOptions = options;
- }
-
- /**
- * Get the options used to create public key credentials.
- *
- * @return the publicKeyCredentialCreationOptions.
- */
- @Nullable public String getPublicKeyCredentialCreationOptions() {
- return publicKeyCredentialCreationOptions;
- }
-
/**
* Set the public key credential request options for authentication.
*
* @param the credential request options to set.
*/
- public void setPublicKeyCredentialRequestOptions(@Nullable final String options) {
+ public WebAuthnAuthenticationContext setPublicKeyCredentialRequestOptions(
+ @Nullable final PublicKeyCredentialRequestOptions options) {
publicKeyCredentialRequestOptions = options;
+ return this;
}
/**
@@ -222,7 +104,7 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
*
* @return the public key credential request options.
*/
- @Nullable public String getPublicKeyCredentialRequestOptions() {
+ @Nullable public PublicKeyCredentialRequestOptions getPublicKeyCredentialRequestOptions() {
return publicKeyCredentialRequestOptions;
}
@@ -231,8 +113,9 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
*
* @param assertion The authenticator assertion response to set.
*/
- public void setAuthenticatorAssertionResponse(@Nullable final String assertion) {
+ public WebAuthnAuthenticationContext setAuthenticatorAssertionResponse(@Nullable final String assertion) {
authenticatorAssertionResponse = assertion;
+ return this;
}
/**
@@ -243,4 +126,30 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
@Nullable public String getAuthenticatorAssertionResponse() {
return authenticatorAssertionResponse;
}
+
+ /**
+ * Get the userId used to map public key credentials to user accounts.
+ *
+ * @return the userHandle.
+ */
+ @Override
+ public byte[] getUserHandle() {
+ return userHandle;
+ }
+
+ /**
+ * @return Returns the publicKeyCredentialRequestOptionsJSON.
+ */
+ public String getPublicKeyCredentialRequestOptionsJSON() {
+ return publicKeyCredentialRequestOptionsJSON;
+ }
+
+ /**
+ * @param publicKeyCredentialRequestOptionsJSON The publicKeyCredentialRequestOptionsJSON to set.
+ */
+ public WebAuthnAuthenticationContext setPublicKeyCredentialRequestOptionsJSON(
+ @Nullable final String requestOptionsJSON) {
+ publicKeyCredentialRequestOptionsJSON = requestOptionsJSON;
+ return this;
+ }
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
new file mode 100644
index 0000000..d2b7348
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
@@ -0,0 +1,142 @@
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+
+
+/** Registration context for processing WebAuthn Registration Ceremonies. */
+ at NotThreadSafe
+public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
+
+ /** The original username. */
+ @Nullable private String username;
+
+ /** In a new context? TODO. But for now an existing credId if found.*/
+ @Deprecated
+ @Nullable private byte[] existingCredentialId;
+
+ /** In a new context? TODO. The existing credential public key encoded in COSE_Key format if found.*/
+ @Deprecated
+ @Nullable private byte[] existingPublicKey;
+
+ /** An assertion response that is the result of an authentication.*/
+ @Nullable private String authenticatorAttestationResponse;
+
+ /** The public key credential creation options for registration.*/
+ @Nullable private PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions;
+
+ /** The public key credential creation options for registration represented as a JSON string.*/
+ @Nullable private String publicKeyCredentialCreationOptionsJSON;
+
+ /**
+ * The credential public key which is the result of registration of a new key pair generated
+ * by the authenticator.
+ */
+ @Nullable private RegistrationResult registrationResult;
+
+
+ /**
+ * Get the attestation response as a result of creating a new credential.
+ *
+ * @return Returns the authenticator attestation response.
+ */
+ @Nullable public String getAuthenticatorAttestationResponse() {
+ return authenticatorAttestationResponse;
+ }
+
+ /**
+ * Set the attestation response as a result of creating a new credential.
+ *
+ * @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
+ */
+ public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(@Nullable final String attestation) {
+ authenticatorAttestationResponse = attestation;
+ 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 WebAuthnRegistrationContext setUsername(@Nullable final String name) {
+ username = name;
+ //transformedUsername = name;
+ return this;
+ }
+
+
+
+
+ /**
+ * Set the options used to create public key credentials.
+ *
+ * @param options The publicKeyCredentialCreationOptions to set.
+ */
+ public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptions(
+ @Nullable final PublicKeyCredentialCreationOptions options) {
+ publicKeyCredentialCreationOptions = options;
+ return this;
+ }
+
+ /**
+ * Get the options used to create public key credentials.
+ *
+ * @return the publicKeyCredentialCreationOptions.
+ */
+ @Nullable public PublicKeyCredentialCreationOptions getPublicKeyCredentialCreationOptions() {
+ return publicKeyCredentialCreationOptions;
+ }
+
+ /**
+ * Set the registration result which is the result of registration of a new key pair generated
+ * by the authenticator.
+ *
+ * @param registrationResult The registrationResult to set.
+ */
+ public WebAuthnRegistrationContext setRegistrationResult(@Nullable final RegistrationResult result) {
+ registrationResult = result;
+ return this;
+ }
+
+ /**
+ * Get the credential public key which is the result of registration of a new key pair generated
+ * by the authenticator
+ *
+ * @return Returns the registrationResult.
+ */
+ public RegistrationResult getRegistrationResult() {
+ return registrationResult;
+ }
+
+ /**
+ * @return Returns the publicKeyCredentialCreationOptionsJSON.
+ */
+ public String getPublicKeyCredentialCreationOptionsJSON() {
+ return publicKeyCredentialCreationOptionsJSON;
+ }
+
+ /**
+ * @param publicKeyCredentialCreationOptionsJSON The publicKeyCredentialCreationOptionsJSON to set.
+ */
+ public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptionsJSON(final String optionsJSON) {
+ publicKeyCredentialCreationOptionsJSON = optionsJSON;
+ return this;
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
similarity index 58%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
index df58513..addcb34 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -13,10 +13,7 @@
*/
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import javax.annotation.Nonnull;
@@ -24,26 +21,57 @@ 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.databind.ObjectMapper;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+
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.AbstractWebAuthnRegistrationAction;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+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;
/**
* Action to create a PublicKeyCredentialCreationOptions from the parameters in the WebAuthn context.
*/
-public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAuthenticationAction {
+public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRegistrationAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialCreationOptions.class);
+ //TODO move out where we do this
+ /** The JSON object mapper used to JSONify webauthn objects. */
+ @NonnullAfterInit private ObjectMapper jsonObjectMapper;
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (jsonObjectMapper == null) {
+ throw new ComponentInitializationException("JSON Object Mapper can not be null");
+ }
+ }
+
+
+ /**
+ * Set the JSON object mapper to use.
+ *
+ * @param mapper The jsonObjectMapper to set.
+ */
+ public void setJsonObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+ jsonObjectMapper = Constraint.isNotNull(mapper, "JsonObjectMapper can not be null");
+ }
+
+
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext context) {
final WebAuthnAuthenticationClient client = getWebAuthnClient();
final byte[] challenge = context.getServerChallenge();
@@ -54,31 +82,19 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAu
}
try {
- final String pkCredCreationOptions =
- client.createRegistrationRequest(context.getUsername(), generateUserHandle(), challenge);
- //verify correct JSON response?
+ final PublicKeyCredentialCreationOptions pkCredCreationOptions =
+ client.createRegistrationRequest(context.getUsername(), context.getUserHandle(), challenge);
context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
+ //convert to JSON
+ context.setPublicKeyCredentialCreationOptionsJSON(
+ jsonObjectMapper.writeValueAsString(pkCredCreationOptions));
+
log.debug("{} Created PublicKeyCredentialCreationOptions '{}'",getLogPrefix(), pkCredCreationOptions);
- } catch (final NoSuchAlgorithmException | WebAuthnAuthenticationClientException e) {
+ } catch (final WebAuthnAuthenticationClientException | JsonProcessingException e) {
log.error("{} Unable to generate PublicKeyCredentialCreationOptions",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
}
}
-
- /**
- * Generate a 32 bytes user handle challenge of sufficient entropy. Must be at maximum
- * 64 bytes long.
- *
- * @return the challenge in bytes
- *
- * @throws NoSuchAlgorithmException if no secure random algorithm is available
- */
- @Nonnull private byte[] generateUserHandle() throws NoSuchAlgorithmException {
- final byte[] bytes = new byte[32];
- SecureRandom.getInstanceStrong().nextBytes(bytes);
- log.trace("{} Generated '{}' byte challenge",getLogPrefix(), bytes.length);
- return bytes;
- }
}
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/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
similarity index 87%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
index 2d893da..30551b9 100644
--- 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/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -28,9 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
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.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -40,15 +39,15 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
- * An action that derives the PublicKeyCredential from a form parameter.
+ * An action that extracts the AuthenticatorAttestationResponse from the incoming HTTP request.
*/
-public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+public class ExtractAuthenticatorAttestationFromFormRequest extends AbstractWebAuthnRegistrationAction {
/** Default token code field name. */
- @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "publicKeyCredential";
+ @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "authenticatorAttestation";
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialFromFormRequest.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractAuthenticatorAttestationFromFormRequest.class);
/** Name of header. */
@Nonnull @NotEmpty private String fieldName;
@@ -57,7 +56,7 @@ public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnA
@NonnullAfterInit private ObjectMapper objectMapper;
/** Constructor. */
- public ExtractPublicKeyCredentialFromFormRequest() {
+ public ExtractAuthenticatorAttestationFromFormRequest() {
fieldName = DEFAULT_FIELD_NAME;
}
@@ -94,8 +93,7 @@ public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnA
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext context) {
final HttpServletRequest request = getHttpServletRequest();
if (request == null) {
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/admin/impl/GenerateUserHandle.java
similarity index 73%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/GenerateUserHandle.java
index bd874f2..9b8bea7 100644
--- 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/admin/impl/GenerateUserHandle.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -27,9 +27,8 @@ 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.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -41,22 +40,20 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
* @post See above.
*/
-public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationAction {
+public class GenerateUserHandle extends AbstractWebAuthnRegistrationAction {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateServerChallenge.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateUserHandle.class);
/** {@inheritDoc} */
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext 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);
+ final byte[] userHandle = generateUserHandle();
+ log.trace("Generated userHandle '{}'",userHandle);
+ context.setUserHandle(userHandle);
} catch (final NoSuchAlgorithmException e) {
log.error("Could not generate a challenge",e);
@@ -65,14 +62,18 @@ public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationActio
}
/**
- * Generate a 32 bytes randomized challenge of sufficient entropy. Must be at least 16 bytes long.
+ * Generate a 64 bytes randomized challenge of sufficient entropy. Must be at least 32 bytes long.
+ *
+ * </p>This could contain some form of state if required, but must not contain retrievable PII.</p>
+ *
+ * @see <a href="https://www.w3.org/TR/webauthn-2/#sctn-user-handle-privacy">user handle</a>
*
* @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];
+ @Nonnull private byte[] generateUserHandle() throws NoSuchAlgorithmException {
+ final byte[] bytes = new byte[64];
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/admin/impl/PopulateWebAuthnRegistrationContext.java
similarity index 53%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
index 9bccc42..588a3b1 100644
--- 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/admin/impl/PopulateWebAuthnRegistrationContext.java
@@ -13,10 +13,9 @@
*/
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.util.function.Function;
-import java.util.function.Predicate;
import javax.annotation.Nonnull;
@@ -26,16 +25,14 @@ 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.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
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}
+ * An action to create (or lookup) and populate the {@link WebAuthnRegistrationContext}
* with the ... FIXME appropriate for this request.
*
* @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
@@ -44,47 +41,38 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
* @post See above.
*/
-public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticationAction {
+public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebauthnAuthenticationContext.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebAuthnRegistrationContext.class);
- /** Strategy used to locate or create the {@link WebauthnAuthenticationContext} to populate. */
- @Nonnull
- private final Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnAuthContextCreationStrategy;
+ /** Strategy used to locate or create the {@link WebAuthnRegistrationContext} to populate. */
+ @Nonnull private
+ Function<ProfileRequestContext,WebAuthnRegistrationContext> webAuthnRegistrationContextCreationStrategy;
- /** Lookup strategy for username to match against Duo identity. */
+ /** Lookup strategy to determine the username to extract and register WebAuthn credentials for. */
@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));
+ public PopulateWebAuthnRegistrationContext() {
+ // Default creates a WebAuthn registration context under the profile request context.
+ webAuthnRegistrationContextCreationStrategy =
+ new ChildContextLookup<>(WebAuthnRegistrationContext.class, true);
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.
+ * Set the strategy used to lookup or create the WebAuthn registration context.
+ *
+ * @param strategy The strategy to set.
*/
- public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
+ public void setWebAuthnRegistrationContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, WebAuthnRegistrationContext> strategy) {
checkSetterPreconditions();
- usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
+ webAuthnRegistrationContextCreationStrategy = Constraint.isNotNull(
+ strategy,"WebAuthnRegistrationContextCreationStrategy can not be null");
}
/**
@@ -101,26 +89,26 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
/** {@inheritDoc} */
- @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext) {
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final WebAuthnAuthenticationContext context = webauthnAuthContextCreationStrategy.apply(profileRequestContext);
+ final WebAuthnRegistrationContext context =
+ webAuthnRegistrationContextCreationStrategy.apply(profileRequestContext);
if (context == null) {
- log.error("{} Error creating WebauthnAuthenticationContext", getLogPrefix());
+ log.error("{} Error creating WebAuthnRegistrationContext", 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());
+ if (username == null) {
+ log.error("{} Error creating WebAuthnRegistrationContext, no username found", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
- log.debug("Created Webauthn authentication context");
+ log.debug("Created WebAuthn registration context for user '{}'", context.getUsername());
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
new file mode 100644
index 0000000..1d5ba1a
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
@@ -0,0 +1,210 @@
+/*
+ * 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.admin.impl;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.TreeSet;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageCapabilities;
+import org.opensaml.storage.StorageSerializer;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.UserIdentity;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnPublicKeyCredentialRecord;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.WebauthnPublicKeyCredentialStorageSerializer;
+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;
+
+public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(StorePublicKeyCredential.class);
+
+ /** Backing service. */
+ @NonnullAfterInit
+ private StorageService storageService;
+
+ /** The credential respository to store valid credentials in.*/
+ // TODO replace with an adaptor to the storage service?
+ @NonnullAfterInit private CredentialRepository credentialRepository;
+
+ /** Storage record serializer. */
+ @Nonnull
+ private final StorageSerializer<WebAuthnPublicKeyCredentialRecord> serializer;
+
+ /** Constructor. */
+ public StorePublicKeyCredential() {
+ serializer = new WebauthnPublicKeyCredentialStorageSerializer();
+ }
+
+ /**
+ * Set the credential repository used to store the valid webauthn credential.
+ *
+ * @param credRepository The credRepository to set.
+ */
+ public void setCredentialRepository(@Nonnull final CredentialRepository repository) {
+ checkSetterPreconditions();
+ credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+ }
+
+
+ /**
+ * 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 (credentialRepository == null) {
+ throw new ComponentInitializationException("Credential respository can not be null");
+ }
+
+ if (storageService == null) {
+ throw new ComponentInitializationException("StorageService cannot be null");
+ }
+ }
+
+ // TODO maybe not needed.
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnRegistrationContext context) {
+ return true;
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnRegistrationContext context) {
+
+ final String username = context.getUsername();
+
+ if (username == null) {
+ log.error("Unable to find username in registration response");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ final RegistrationResult registrationResult = context.getRegistrationResult();
+ if (registrationResult == null) {
+ log.error("Unable to find registration information in registration response");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+ try {
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(registrationResult.getKeyId().getId())
+ .userHandle(ByteArray.fromBase64("bm90YWhhbmRsZQ=="))
+ .publicKeyCose(registrationResult.getPublicKeyCose())
+ .build();
+
+ final UserIdentity user = UserIdentity.builder()
+ .name(username)
+ .displayName(username)
+ .id(ByteArray.fromBase64("bm90YWhhbmRsZQ=="))
+ .build();
+
+ // TODO fixup the record we will use to store registrations
+ final CredentialRegistration registration = new CredentialRegistration(user, Optional.of("Nickanme"),
+ new TreeSet<AuthenticatorTransport>(), Instant.now(), credential, Optional.empty());
+
+ // TODO should not need a cast here when we sort out the storage
+ if (credentialRepository instanceof final InMemoryRegistrationStorage inMemoryRepo) {
+ inMemoryRepo.addRegistrationByUsername(username, registration);
+ log.debug("{} Added public key credential registration for user '{}' and key '{}' ",
+ getLogPrefix(), username, registrationResult.getKeyId().getId().getBase64Url());
+ } else {
+ log.debug("{} Unsupported credential repository type", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ } catch (final Exception e) {
+ log.error("{} Unable to store registration for key '{}'",getLogPrefix(),
+ registrationResult.getKeyId().getId().getBase64Url(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_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/impl/ValidateAuthenticatorAttestationResponse.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
similarity index 63%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
index cee6f8f..12e3bc2 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import javax.annotation.Nonnull;
@@ -23,26 +23,29 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+
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.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.RegistrationFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.primitive.StringSupport;
/**
- * Validate the public key registration attempt.
+ * Validate the public key registration attempt. If valid store it inside the credential repository.
*/
-public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAuthenticationAction {
+public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnRegistrationAction {
/** Class logger. */
@Nonnull
private final Logger log = LoggerFactory.getLogger(ValidateAuthenticatorAttestationResponse.class);
/** The stashed public key credential creation options used to create a new credential.*/
- @NonnullBeforeExec @NotEmpty private String pkCredCreationOptions;
+ @NonnullBeforeExec @NotEmpty private PublicKeyCredentialCreationOptions pkCredCreationOptions;
/** The stashed authenticator response.*/
@NonnullBeforeExec @NotEmpty private String attestation;
@@ -51,8 +54,11 @@ public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAu
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext context) {
+
+ if (!super.doPreExecute(profileRequestContext, context)) {
+ return false;
+ }
attestation = context.getAuthenticatorAttestationResponse();
if (StringSupport.trimOrNull(attestation) == null) {
@@ -62,39 +68,30 @@ public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAu
}
pkCredCreationOptions = context.getPublicKeyCredentialCreationOptions();
- if (StringSupport.trimOrNull(pkCredCreationOptions) == null) {
+ if (pkCredCreationOptions == null) {
log.error("{} public key credential creation options was null", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return false;
}
- return super.doPreExecute(profileRequestContext, authenticationContext, context);
+ return true;
}
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Nonnull final WebAuthnRegistrationContext context) {
- // TODO this should throw the error?
- final boolean publicKeyCredentialIsValid =
- getWebAuthnClient().validateRegistration(pkCredCreationOptions, attestation);
-
- if (!publicKeyCredentialIsValid) {
- log.error("{} public key credential creation options was invalid", getLogPrefix());
+ try {
+ final RegistrationResult credentialPublicKey =
+ getWebAuthnClient().validateAuthenticatorAttestationResponse(pkCredCreationOptions, attestation);
+ // If valid. Add back to context
+ context.setRegistrationResult(credentialPublicKey);
+ log.info("Public Key Registration was valid");
+ } catch (final RegistrationFailureException e) {
+ log.error("{} public key credential creation options was invalid", getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return;
}
-
- // 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/client/impl/YubicoWebauthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
index f78bad6..a0b268e 100644
--- 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
@@ -26,11 +26,12 @@ import javax.annotation.concurrent.ThreadSafe;
import org.slf4j.Logger;
-import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yubico.webauthn.AssertionRequest;
+import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.FinishAssertionOptions;
import com.yubico.webauthn.FinishRegistrationOptions;
+import com.yubico.webauthn.RegistrationResult;
import com.yubico.webauthn.RelyingParty;
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.ByteArray;
@@ -41,16 +42,17 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import com.yubico.webauthn.data.UserIdentity;
import com.yubico.webauthn.data.UserVerificationRequirement;
-import com.yubico.webauthn.exception.AssertionFailedException;
import com.yubico.webauthn.exception.RegistrationFailedException;
+import net.shibboleth.idp.plugin.authn.webauthn.AssertionFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.RegistrationFailureException;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * Yuibico implementation of a {@link WebAuthnAuthenticationClient}.
+ * Yubico implementation of a {@link WebAuthnAuthenticationClient}.
*
* <p>Thread-safe, only a single instance is required.</p>
*/
@@ -92,24 +94,23 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
}
@Override
- public String createAuthenticationRequest(@Nullable final String username, final byte[] userHandle,
- final byte[] challenge) throws WebAuthnAuthenticationClientException {
- 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;
- }
+ public PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username,
+ final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
+
+ //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 PublicKeyCredentialRequestOptions pkcro =
- PublicKeyCredentialRequestOptions.builder()
- .challenge(new ByteArray(challenge))
- .rpId(rp.getIdentity().getId())
+ final PublicKeyCredentialRequestOptions request = PublicKeyCredentialRequestOptions.builder()
+ .challenge(new ByteArray(challenge))
+ .rpId(rp.getIdentity().getId())
// .allowCredentials(
// OptionalUtil.orElseOptional(
// startAssertionOptions.getUsername(),
@@ -124,38 +125,36 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
// startAssertionOptions
// .getExtensions()
// .merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
- .userVerification(userVerificationRequirement)
- .timeout(Optional.of(60000l))
- .build();
-
- return om.writerWithDefaultPrettyPrinter().writeValueAsString(pkcro);
+ .userVerification(userVerificationRequirement)
+ .timeout(Optional.of(60000l))
+ .build();
+ if (request == null) {
+ throw new WebAuthnAuthenticationClientException("Unable to build public key credential request options");
}
- catch (final JsonProcessingException e) {
- throw new WebAuthnAuthenticationClientException(e);
- }
+ return request;
+
}
/** {@inheritDoc} */
@Override
- public String createRegistrationRequest(final String username, final byte[] userHandle, final byte[] challenge)
- throws WebAuthnAuthenticationClientException {
- 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 UserIdentity identity =
- UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
-
- final PublicKeyCredentialCreationOptions options =
- PublicKeyCredentialCreationOptions.builder()
- .rp(rp.getIdentity())
- .user(identity)
- .challenge(new ByteArray(challenge))
- .pubKeyCredParams(preferredPublickeyParams)
- .excludeCredentials(Optional.empty())
+ public PublicKeyCredentialCreationOptions createRegistrationRequest(final String username,
+ final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
+
+ //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 UserIdentity identity =
+ UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
+
+ final PublicKeyCredentialCreationOptions creation = PublicKeyCredentialCreationOptions.builder()
+ .rp(rp.getIdentity())
+ .user(identity)
+ .challenge(new ByteArray(challenge))
+ .pubKeyCredParams(preferredPublickeyParams)
+ .excludeCredentials(Optional.empty())
// .excludeCredentials(
// credentialRepository.getCredentialIdsForUsername(
// startRegistrationOptions.getUser().getName()))
@@ -168,28 +167,23 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
// .appidExclude(appId)
// .credProps()
// .build()))
- .timeout(Optional.empty()).build();
- return om.writerWithDefaultPrettyPrinter().writeValueAsString(options);
+ .timeout(Optional.empty()).build();
+ if (creation == null) {
+ throw new WebAuthnAuthenticationClientException("Unable to build public key credential creation options");
}
- catch (final JsonProcessingException e) {
- throw new WebAuthnAuthenticationClientException(e);
- }
+ return creation;
}
@Override
- public boolean validateAuthenticatorAssertionResponse(@Nullable final String username,
+ public AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username,
@Nullable final byte[] userHandle,
- @Nonnull final String publicKeyCredentialRequestOptions,
- @Nonnull final String authenticatorAssertionResponse) throws WebAuthnAuthenticationClientException {
+ @Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
+ @Nonnull final String authenticatorAssertionResponse) throws AssertionFailureException {
try {
- final PublicKeyCredentialRequestOptions request =
- om.readValue(publicKeyCredentialRequestOptions, PublicKeyCredentialRequestOptions.class);
-
final AssertionRequest requestAssertion = AssertionRequest.builder()
- .publicKeyCredentialRequestOptions(request)
- // TODO these
+ .publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions)
.userHandle(Optional.ofNullable(userHandle != null ? new ByteArray(userHandle) : null))
.username(Optional.ofNullable(username))
.build();
@@ -198,49 +192,43 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
PublicKeyCredential.parseAssertionResponseJson(authenticatorAssertionResponse);
log.trace("Client Data '{}'",pkCred.getResponse().getClientData());
log.trace("Signature '{}'",pkCred.getResponse().getSignature());
- rp.finishAssertion(FinishAssertionOptions.builder()
+ final AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(requestAssertion)
.response(pkCred)
.build());
+ if (result == null) {
+ throw new AssertionFailureException("Unable to validate authenticator assertion");
+ }
+ if (!result.isSuccess()) {
+ // TODO why do they use both success and throw an exception?
+ // I think this is *always* true if valid, and will throw if not valid. But just in case.
+ throw new AssertionFailureException("Authenticator assertion was not valid");
+ }
+ return result;
- } catch (final AssertionFailedException e) {
- log.error("Public key assertion (authentication) is not valid", e);
- return false;
- } catch (final Exception e) {
- throw new WebAuthnAuthenticationClientException(e);
- }
-
- return true;
+ } catch (final Exception e) {
+ throw new AssertionFailureException(e);
+ }
}
/** {@inheritDoc} */
@Override
- public boolean validateRegistration(final String publicKeyCredentialCreationOptions,
- final String authenticatorAttestationResponse) {
+ public RegistrationResult validateAuthenticatorAttestationResponse(
+ @Nonnull final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions,
+ @Nonnull final String authenticatorAttestationResponse) throws RegistrationFailureException {
- try {
- final PublicKeyCredentialCreationOptions requestOptions =
- PublicKeyCredentialCreationOptions.fromJson(publicKeyCredentialCreationOptions);
-
- log.debug("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
+ try {
+ log.trace("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
final var publicKeyRegistration =
- PublicKeyCredential.parseRegistrationResponseJson(authenticatorAttestationResponse);
-
+ PublicKeyCredential.parseRegistrationResponseJson(authenticatorAttestationResponse);
- rp.finishRegistration(FinishRegistrationOptions.builder()
- .request(requestOptions)
+ return rp.finishRegistration(FinishRegistrationOptions.builder()
+ .request(publicKeyCredentialCreationOptions)
.response(publicKeyRegistration)
.build());
- // TODO do we need the result?
+
} catch (final RegistrationFailedException | IOException e) {
- log.error("Public key attestation (registration) is not valid", e);
- // Should throw this?
- return false;
+ throw new RegistrationFailureException(e);
}
- 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
index a03463d..522c09c 100644
--- 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
@@ -19,15 +19,15 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
-import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.FactoryBean;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.CredentialRepository;
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.InMemoryRegistrationStorage;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.component.AbstractInitializableComponent;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
@@ -35,7 +35,6 @@ 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.
@ThreadSafe
public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
implements FactoryBean<WebAuthnAuthenticationClient> {
@@ -53,7 +52,11 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
@GuardedBy("this") private boolean allowOriginSubdomain;
/** The JSON object mapper used to JSONify webauthn objects. */
- @GuardedBy("this") @Nullable private ObjectMapper om;
+ @GuardedBy("this") @Nullable private ObjectMapper om;
+
+ /** The credential repository to store valid credentials in.*/
+ // TODO replace with an adaptor to the storage service?
+ @GuardedBy("this") @NonnullAfterInit private CredentialRepository credentialRepository;
/** Constructor.*/
public YubicoWebauthnClientFactory() {
@@ -73,10 +76,12 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
if (om == null) {
throw new ComponentInitializationException("ObjectMapper cannot be null");
}
+ if (credentialRepository == null) {
+ throw new ComponentInitializationException("Credential repository cannot be null");
+ }
}
-
@Override
public WebAuthnAuthenticationClient getObject() throws Exception {
final RelyingParty rp = RelyingParty.builder().identity(
@@ -85,18 +90,35 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
.id(getRelyingPartyId())
.name(getRelyingPartyName())
//Use inmemory for now
- .build()).credentialRepository(new InMemoryRegistrationStorage())
+ .build()).credentialRepository(getCredentialRepository())
.allowOriginPort(isAllowOriginPort())
.allowOriginSubdomain(isAllowOriginSubdomain())
.build();
assert rp != null;
- final ObjectMapper localMapper = getObjectMapper();
- if (localMapper == null) {
- // Should not happen after init
- throw new BeanInitializationException("Object mapper can not be null");
- }
- return new YubicoWebauthnAuthenticationClient(rp,localMapper);
+ return new YubicoWebauthnAuthenticationClient(rp, getObjectMapper());
+ }
+
+ /**
+ * Get the credential repository used to store the valid webauthn credential.
+ *
+ * @return the credential repository.
+ */
+ @Nonnull public synchronized CredentialRepository getCredentialRepository() {
+ checkComponentActive();
+ assert credentialRepository != null;
+ return credentialRepository;
+ }
+
+
+ /**
+ * Set the credential repository used to store the valid webauthn credential.
+ *
+ * @param repository The credential repository to set.
+ */
+ public synchronized void setCredentialRepository(@Nonnull final CredentialRepository repository) {
+ checkSetterPreconditions();
+ credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
}
@Override
@@ -124,8 +146,9 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
*
* @return the objectMapper;
*/
- private synchronized ObjectMapper getObjectMapper() {
+ @Nonnull private synchronized ObjectMapper getObjectMapper() {
checkComponentActive();
+ assert om != null;
return om;
}
/**
@@ -211,8 +234,6 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
checkSetterPreconditions();
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
index 12ed08e..5e6c42f 100644
--- 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
@@ -21,12 +21,19 @@ 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.databind.ObjectMapper;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
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.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
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;
/**
@@ -37,6 +44,31 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialRequestOptions.class);
+ //TODO move out where we do this
+ /** The JSON object mapper used to JSONify webauthn objects. */
+ @NonnullAfterInit private ObjectMapper jsonObjectMapper;
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (jsonObjectMapper == null) {
+ throw new ComponentInitializationException("JSON Object Mapper can not be null");
+ }
+ }
+
+
+ /**
+ * Set the JSON object mapper to use.
+ *
+ * @param mapper The jsonObjectMapper to set.
+ */
+ public void setJsonObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+ jsonObjectMapper = Constraint.isNotNull(mapper, "JsonObjectMapper can not be null");
+ }
+
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext,
@@ -53,13 +85,14 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
try {
//TODO userhandle needs to be pulled out.
- final String pkCredRequestOptions = client.createAuthenticationRequest(context.getUsername(),
- null, challenge);
- //verify correct JSON response?
+ final PublicKeyCredentialRequestOptions pkCredRequestOptions =
+ client.createAuthenticationRequest(context.getUsername(), null, challenge);
context.setPublicKeyCredentialRequestOptions(pkCredRequestOptions);
+ context.setPublicKeyCredentialRequestOptionsJSON(jsonObjectMapper.writeValueAsString(pkCredRequestOptions));
+
log.debug("{} Created PublicKeyCredentialRequestOptions: '{}'",getLogPrefix(), pkCredRequestOptions);
- } catch (final WebAuthnAuthenticationClientException e) {
+ } catch (final WebAuthnAuthenticationClientException | JsonProcessingException e) {
log.error("{} Unable to generate PublicKeyCredentialRequestOptions",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
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/ExtractAuthenticatorAssertionFromFormRequest.java
similarity index 81%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
index e55c766..df8fc73 100644
--- 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/ExtractAuthenticatorAssertionFromFormRequest.java
@@ -40,15 +40,15 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
- * An action that derives the PublicKeyAssertion from a form parameter.
+ * An action that extracts the AuthenticatorAssertionResponse from the incoming HTTP request.
*/
-public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+public class ExtractAuthenticatorAssertionFromFormRequest 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);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractAuthenticatorAssertionFromFormRequest.class);
/** Name of header. */
@NonnullAfterInit @NotEmpty private String fieldName;
@@ -57,7 +57,7 @@ public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAu
@NonnullAfterInit private ObjectMapper objectMapper;
/** Constructor. */
- public ExtractPublicKeyAssertionFromFormRequest() {
+ public ExtractAuthenticatorAssertionFromFormRequest() {
fieldName = DEFAULT_FIELD_NAME;
}
@@ -103,18 +103,26 @@ public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAu
return;
}
- final String pkCredJson = extractAuthenticatorResponse(request);
- log.trace("Public Key Assertion in JSON is '{}'",pkCredJson);
+ final String pkCredJson = extractAuthenticatorAssertionResponse(request);
if (pkCredJson == null) {
+ log.debug("Public key assertion not found in HTTP request: '{}'",pkCredJson);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return;
- }
+ }
+ log.trace("Public key assertion found: '{}'",pkCredJson);
context.setAuthenticatorAssertionResponse(pkCredJson);
}
- @Nullable private String extractAuthenticatorResponse(@Nonnull final HttpServletRequest httpRequest) {
+ /**
+ * Extract the public key assertion response from the HTTP request parameters.
+ *
+ * @param httpRequest the http request
+ *
+ * @return the raw, unformatted, authenticator assertion response
+ */
+ @Nullable private String extractAuthenticatorAssertionResponse(@Nonnull final HttpServletRequest httpRequest) {
return httpRequest.getParameter(fieldName);
}
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
index bd874f2..a6b8490 100644
--- 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
@@ -19,17 +19,21 @@ package net.shibboleth.idp.plugin.authn.webauthn.impl;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+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.action.EventIds;
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.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -41,26 +45,60 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
* @post See above.
*/
-public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationAction {
+public class GenerateServerChallenge extends AbstractProfileAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(GenerateServerChallenge.class);
+
+ /** Lookup strategy to locate the WebAuthn base context. */
+ @Nonnull private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnBaseContextLookupStrategy;
+
+ @NonnullBeforeExec private BaseWebAuthnContext context;
+
+ /** Constructor.*/
+ protected GenerateServerChallenge() {
+ //prc -> ac -> base context
+ webAuthnBaseContextLookupStrategy = new ChildContextLookup<>(BaseWebAuthnContext.class).
+ compose(new ChildContextLookup<>(AuthenticationContext.class));
+ }
+
+ /**
+ * @param webauthnBaseContextLookupStrategy The webauthnBaseContextLookupStrategy to set.
+ */
+ public void setWebAuthnBaseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, BaseWebAuthnContext> strategy) {
+ checkSetterPreconditions();
+ webAuthnBaseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "WebAuthnBaseContextLookupStrategy can not be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+ context = webAuthnBaseContextLookupStrategy.apply(profileRequestContext);
+ if (context == null) {
+ log.warn("{} WebAuthnBaseContext is not available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ return true;
+ }
/** {@inheritDoc} */
- @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final WebAuthnAuthenticationContext context) {
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
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);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
}
}
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
similarity index 96%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java
index 9bccc42..9849f6d 100644
--- 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
@@ -44,10 +44,10 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
* @post See above.
*/
-public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticationAction {
+public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticationAction {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebauthnAuthenticationContext.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebAuthnAuthenticationContext.class);
/** Strategy used to locate or create the {@link WebauthnAuthenticationContext} to populate. */
@Nonnull
@@ -61,7 +61,7 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
/** Constructor.*/
- public PopulateWebauthnAuthenticationContext() {
+ public PopulateWebAuthnAuthenticationContext() {
//default creates webauthn authentication context under authentication context.
webauthnAuthContextCreationStrategy =
new ChildContextLookup<>(WebAuthnAuthenticationContext.class, true).
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
index e76de02..293eb15 100644
--- 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
@@ -10,12 +10,15 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
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.AssertionFailureException;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
-import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
@@ -24,7 +27,9 @@ import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * See https://w3c.github.io/webauthn/#sctn-verifying-assertion
+ * An action that validates a WebAuthn Authenticator Assertion that results from a call to 'get' (authentication).
+ *
+ * @see https://w3c.github.io/webauthn/#sctn-verifying-assertion
*/
public class ValidateWebAuthnAssertion extends AbstractValidationAction {
@@ -43,7 +48,7 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
@NonnullAfterInit private WebAuthnAuthenticationClient webAuthnClient;
/** The options used to create the authentication request.*/
- @NonnullBeforeExec private String publicKeyCredentialRequestOptions;
+ @NonnullBeforeExec private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
/** Constructor. */
public ValidateWebAuthnAssertion() {
@@ -100,7 +105,9 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
final String assertion = context.getAuthenticatorAssertionResponse();
+ // TODO these need to be set from lookup earlier in the context
context.setUsername("not-the-username");
+ context.setUserHandle("notahandle".getBytes());
if (assertion == null) {
log.warn("{} No authenticator assertion found, {} can not authenticate ",
@@ -111,19 +118,12 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
return;
}
try {
- final boolean valid = webAuthnClient.validateAuthenticatorAssertionResponse(context.getUsername(),
- null, publicKeyCredentialRequestOptions, assertion);
- if (!valid) {
- log.warn("{} Authenticator assertion was not valid for '{}', authentication failed",
- getLogPrefix(),context.getUsername());
- handleError(profileRequestContext, authenticationContext, AuthnEventIds.NO_CREDENTIALS,
- AuthnEventIds.INVALID_CREDENTIALS);
- recordFailure(profileRequestContext);
- return;
- } else {
- log.debug("{} Authenticator assertion was valid for '{}'", getLogPrefix(), context.getUsername());
- }
- } catch (final WebAuthnAuthenticationClientException e) {
+ final AssertionResult result = webAuthnClient.validateAuthenticatorAssertionResponse(
+ context.getUsername(), context.getUserHandle(), publicKeyCredentialRequestOptions, assertion);
+
+ buildAuthenticationResult(profileRequestContext, authenticationContext);
+
+ } catch (final AssertionFailureException e) {
log.warn("{} Error validating authenticator assertion for '{}'",
getLogPrefix(),context.getUsername(), e);
handleError(profileRequestContext, authenticationContext, "InvalidResponseType",
@@ -131,8 +131,6 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
recordFailure(profileRequestContext);
return;
}
-
- buildAuthenticationResult(profileRequestContext, authenticationContext);
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialPublicKeyHolder.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialPublicKeyHolder.java
new file mode 100644
index 0000000..91ecf95
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialPublicKeyHolder.java
@@ -0,0 +1,91 @@
+/*
+ * 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.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.authn.webauthn.CredentialPublicKey;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A holder for the {@link CredentialPublicKey} which includes additional information required for storage of the
+ * public key information.
+ */
+public class CredentialPublicKeyHolder {
+
+ /**
+ * The credential public key component of the credential key pair created by an authenticator during
+ * registration.
+ */
+ @Nonnull private final CredentialPublicKey credentialPublicKey;
+
+ @Nonnull private final Instant registrationTime;
+
+ @Nonnull private final String username;
+
+ /** The user ID as specified by the IdP that maps a credential public key to a user. */
+ @Nonnull private final byte[] userHandle;
+
+ //SortedSet<AuthenticatorTransport> transports;
+
+ //Optional<Object> attestationMetadata;
+
+
+ /**
+ * Constructor.
+ *
+ * @param publicKey the credential public key
+ */
+ public CredentialPublicKeyHolder(@Nonnull final CredentialPublicKey publicKey,
+ @Nonnull final Instant regTime, @Nonnull @NotEmpty final String uname,
+ @Nonnull final byte[] id) {
+ credentialPublicKey = Constraint.isNotNull(publicKey, "Credential public key can not be null");
+ registrationTime = Constraint.isNotNull(regTime, "Registration time can not be null");
+ username = Constraint.isNotEmpty(uname, "Username can not be null or empty");
+ userHandle = Constraint.isNotEmpty(id, "UserHandle (ID) can not be null or empty");
+ }
+
+ /**
+ * Get the credential public key.
+ *
+ * @return Returns the credentialPublicKey.
+ */
+ public CredentialPublicKey getCredentialPublicKey() {
+ return credentialPublicKey;
+ }
+
+ @Nonnull public String getRegistrationTimestamp() {
+ return registrationTime.toString();
+ }
+
+ @Nonnull public byte[] getId() {
+ return userHandle;
+ }
+
+ @Nonnull public String getUsername() {
+ return username;
+ }
+
+ @Nonnull public CredentialPublicKey getCredential() {
+ return credentialPublicKey;
+ }
+
+// public SortedSet<AuthenticatorTransport> getTransports() {
+// return transports;
+// }
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java
index 9c70e3f..1a0a0cc 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java
@@ -87,5 +87,6 @@ public class CredentialRegistration {
newReg.credential = newRegCred;
return newReg;
}
+
}
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
deleted file mode 100644
index 5952ac9..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 6498db1..89ffa59 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
@@ -83,15 +83,22 @@
</property>
</bean>
- <!-- Singleton clients -->
+ <!-- Singleton clients and repositories -->
+ <!-- TODO configure these with getbeans and properties -->
<bean id="shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory" scope="singleton"
class="net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory"
p:relyingPartyId="%{idp.authn.webauthn.relyingPartyId}"
p:relyingPartyName="Shibboleth"
p:allowOriginPort ="%{idp.authn.webauthn.allowOriginPort:false}"
p:allowOriginSubdomain ="%{idp.authn.webauthn.allowOriginSubdomain:false}"
- p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
-
+ p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper"
+ p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository"/>
+
+ <!-- TODO replace with our own -->
+ <bean id="shibboleth.authn.webauthn.DefaultCredentialRepository" scope="singleton"
+ class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage"/>
+
+
<!--
Create a default object mapper. Setup should not change once injected.
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
index 77f0484..4734659 100644
--- 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
@@ -12,35 +12,46 @@
<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">
+ <!-- TODO Should this been populating an authentication context for an admin flow? -->
+ <bean id="PopulateWebAuthnRegistrationContext" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
<property name="usernameLookupStrategy">
- <bean id="UsernameFromAuthenticationContextLookupStrategy"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy"/>
+ <bean id="UsernameFromAuthenticationContextLookupStrategy"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy" />
</property>
</bean>
- <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnAuthenticationAction"
- class=" net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialCreationOptions"/>
-
- <bean id="ExtractPublicKeyCredentialFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialFromFormRequest"
+ <bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
+
+ <bean id="GenerateServerChallenge" parent="AbstractGenerateServerChallenge"
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
+
+
+ <bean id="GenerateUserHandle" parent="AbstractWebAuthnAuthenticationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.GenerateUserHandle" />
+
+ <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction"
+ class=" net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions" />
+
+ <bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractAuthenticatorAttestationFromFormRequest"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
-
-
- <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnAuthenticationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateAuthenticatorAttestationResponse" />
-
-
- <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnAuthenticationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.StorePublicKeyCredential"
- p:storageService-ref="shibboleth.authn.webauthn.StorageService"/>
-
- <!-- postconfig.xml? -->
+
+
+ <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
+
+
+ <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.StorePublicKeyCredential"
+ p:storageService-ref="shibboleth.authn.webauthn.StorageService"
+ p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository" />
+
+ <!-- postconfig.xml? -->
<bean id="shibboleth.authn.webauthn.StorageService" lazy-init="true"
- class="org.opensaml.storage.impl.MemoryStorageService"
- p:cleanupInterval="PT10M" />
-
+ 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
index 6d86fb1..ecbef86 100644
--- 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
@@ -24,8 +24,9 @@
</action-state>
<action-state id="PopulateWebAuthnContext">
- <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+ <evaluate expression="PopulateWebAuthnRegistrationContext"/>
<evaluate expression="GenerateServerChallenge"/>
+ <evaluate expression="GenerateUserHandle"/>
<evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
<evaluate expression="'proceed'" />
<transition on="proceed" to="DisplayWebAuthnView" />
@@ -36,7 +37,7 @@
<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="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
<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" />
@@ -49,8 +50,8 @@
</view-state>
<action-state id="ExtractPublicKeyCredential">
- <evaluate expression="ExtractPublicKeyCredentialFromFormRequest"/>
- <evaluate expression="ValidatePublicKeyCredential"/>
+ <evaluate expression="ExtractAuthenticatorAttestationFromFormRequest"/>
+ <evaluate expression="ValidateAuthenticatorAttestationResponse"/>
<evaluate expression="StorePublicKeyCredential"/>
<evaluate expression="'proceed'" />
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
index 338d3d2..8e6b455 100644
--- 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
@@ -9,15 +9,15 @@
default-init-method="initialize" default-destroy-method="destroy">
- <!-- Parent beans -->
- <bean id="AbstractPopulateWebauthnAuthenticationContext" scope="prototype" abstract="true"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebauthnAuthenticationContext"/>
-
+ <!-- Parent beans -->
<bean id="AbstractWebAuthnAuthenticationAction" scope="prototype" abstract="true"
p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
+
+ <bean id="AbstractWebAuthnRegistrationAction" scope="prototype" abstract="true"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
<!-- Generic beans -->
- <bean id="GenerateServerChallenge" parent="AbstractWebAuthnAuthenticationAction"
+ <bean id="AbstractGenerateServerChallenge" scope="prototype" abstract="true"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge" />
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 24012c2..6a99cc6 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
@@ -9,16 +9,27 @@
default-init-method="initialize" default-destroy-method="destroy">
- <bean id="PopulateWebauthnAuthenticationContext" scope="prototype"
- parent="AbstractPopulateWebauthnAuthenticationContext"
+ <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
p:usernameRequiredPredicate="false">
</bean>
+ <bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext) }" />
+
+ <bean id="GenerateServerChallenge" parent="AbstractGenerateServerChallenge">
+ <property name="webAuthnBaseContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
+ c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
+ </property>
+ </bean>
+
<bean id="CreatePublicKeyCredentialRequestOptions" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"/>
- <bean id="ExtractPublicKeyAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyAssertionFromFormRequest"
+ <bean id="ExtractAuthenticatorAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractAuthenticatorAssertionFromFormRequest"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
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 5942fec..dc26b5c 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
@@ -3,7 +3,7 @@
parent="authn.abstract, authn/conditions">
<action-state id="PopulateWebauthnContext">
- <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+ <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
<evaluate expression="GenerateServerChallenge"/>
<evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
<evaluate expression="'proceed'" />
@@ -29,7 +29,7 @@
</view-state>
<action-state id="AuthenticatePublicKeyCredential">
- <evaluate expression="ExtractPublicKeyAssertionFromFormRequest"/>
+ <evaluate expression="ExtractAuthenticatorAssertionFromFormRequest"/>
<evaluate expression="ValidateWebAuthnAssertion"/>
<evaluate expression="'proceed'" />
<transition on="proceed" to="proceed" />
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
index 69ddf0d..d07a1da 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
@@ -14,7 +14,7 @@
package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
-import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.time.Instant;
@@ -29,7 +29,9 @@ import org.slf4j.Logger;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RegistrationResult;
import com.yubico.webauthn.RelyingParty;
import com.yubico.webauthn.data.AuthenticatorTransport;
import com.yubico.webauthn.data.ByteArray;
@@ -39,6 +41,8 @@ import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.data.UserIdentity;
import com.yubico.webauthn.data.UserVerificationRequirement;
+import net.shibboleth.idp.plugin.authn.webauthn.AssertionFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.RegistrationFailureException;
import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.Attestation;
@@ -72,9 +76,9 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
private YubicoWebauthnAuthenticationClient client;
- private String credentialCreationOptions;
+ private PublicKeyCredentialCreationOptions credentialCreationOptions;
- private String credentialRequestOptions;
+ private PublicKeyCredentialRequestOptions credentialRequestOptions;
@@ -105,7 +109,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
UserIdentity.builder().name(USERNAME).displayName("test user")
.id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
- final PublicKeyCredentialCreationOptions options =
+ credentialCreationOptions =
PublicKeyCredentialCreationOptions.builder()
.rp(rp.getIdentity())
.user(userIdentity)
@@ -113,11 +117,8 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
.pubKeyCredParams(preferredPublickeyParams)
.excludeCredentials(Optional.empty())
.timeout(Optional.empty()).build();
- credentialCreationOptions =
- jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
-
-
- final PublicKeyCredentialRequestOptions pkcro =
+
+ credentialRequestOptions =
PublicKeyCredentialRequestOptions.builder()
.challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
.rpId(rp.getIdentity().getId())
@@ -125,8 +126,6 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
.timeout(Optional.of(60000l))
.build();
- credentialRequestOptions = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(pkcro);
-
}
@@ -142,13 +141,14 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
Base64Support.decode(USER_HANDLE_B64));
final var attestationJson = jsonMapper.writeValueAsString(attestation);
- final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+ final RegistrationResult registration =
+ client.validateAuthenticatorAttestationResponse(credentialCreationOptions, attestationJson);
- assertTrue(registrationIsValid);
+ assertNotNull(registration);
}
- @Test
+ @Test(expectedExceptions = RegistrationFailureException.class)
public void testValidateRegistration_Fail_BadRpId() throws Exception {
mockAuthenticator = new MockAuthenticator("wrong-rpid.example.com");
@@ -160,12 +160,13 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
Base64Support.decode(USER_HANDLE_B64));
final var attestationJson = jsonMapper.writeValueAsString(attestation);
- final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+ final RegistrationResult registration =
+ client.validateAuthenticatorAttestationResponse(credentialCreationOptions, attestationJson);
- assertFalse(registrationIsValid);
+ assertNotNull(registration);
}
- @Test
+ @Test(expectedExceptions = RegistrationFailureException.class)
public void testValidateRegistrationFail_BadOrigin() throws Exception {
mockAuthenticator = new MockAuthenticator(RPID);
@@ -177,9 +178,9 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
Base64Support.decode(USER_HANDLE_B64));
final var attestationJson = jsonMapper.writeValueAsString(attestation);
- final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+ final RegistrationResult registration =
+ client.validateAuthenticatorAttestationResponse(credentialCreationOptions, attestationJson);
- assertFalse(registrationIsValid);
}
@Test
@@ -213,14 +214,15 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
final var assertionJson = jsonMapper.writeValueAsString(assertion);
log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
- final boolean valid =
+ final AssertionResult result =
client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertionJson);
- assertTrue(valid);
+ assertNotNull(result);
+ assertTrue(result.isSuccess());
}
- @Test
+ @Test(expectedExceptions = AssertionFailureException.class)
public void testValidateAuthentication_Fail_WrongOrigin() throws Exception {
mockAuthenticator = new MockAuthenticator(RPID);
@@ -252,14 +254,12 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
final var assertionJson = jsonMapper.writeValueAsString(assertion);
log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
- final boolean valid =
- client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertionJson);
- assertFalse(valid);
}
- @Test
+ @Test(expectedExceptions = AssertionFailureException.class)
public void testValidateAuthentication_Fail_WrongOperationType() throws Exception {
mockAuthenticator = new MockAuthenticator(RPID);
@@ -291,14 +291,12 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
final var assertionJson = jsonMapper.writeValueAsString(assertion);
log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
- final boolean valid =
- client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertionJson);
- assertFalse(valid);
-
+
}
- @Test
+ @Test(expectedExceptions = AssertionFailureException.class)
public void testValidateAuthentication_Fail_BadSignature_DifferentKey() throws Exception {
mockAuthenticator = new MockAuthenticator(RPID);
@@ -331,10 +329,8 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
final var assertionJson = jsonMapper.writeValueAsString(assertion);
log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
- final boolean valid =
- client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertionJson);
- assertFalse(valid);
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
index 8abc3cb..5e03116 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -32,6 +32,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
@@ -47,6 +48,9 @@ public abstract class AbstractWebAuthnTest {
/** The webauthn authentication context.*/
protected WebAuthnAuthenticationContext webAuthnContext;
+ /** The webauthn registration context.*/
+ protected WebAuthnRegistrationContext webAuthnRegContext;
+
/** The authentication context.*/
protected AuthenticationContext ac;
@@ -94,6 +98,9 @@ public abstract class AbstractWebAuthnTest {
prc.addSubcontext(ac);
assert null != webAuthnContext;
ac.addSubcontext(webAuthnContext);
+
+ webAuthnRegContext = new WebAuthnRegistrationContext();
+ prc.addSubcontext(webAuthnRegContext);
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
index 9634247..8b6a85c 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
@@ -26,6 +26,7 @@ import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.data.UserIdentity;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse;
import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
import net.shibboleth.shared.codec.Base64Support;
@@ -62,7 +63,7 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
UserIdentity.builder().name("test-user").displayName("test user")
.id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
- final PublicKeyCredentialCreationOptions options =
+ final PublicKeyCredentialCreationOptions credentialCreationOptions =
PublicKeyCredentialCreationOptions.builder()
.rp(rp.getIdentity())
.user(identity)
@@ -70,9 +71,8 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
.pubKeyCredParams(preferredPublickeyParams)
.excludeCredentials(Optional.empty())
.timeout(Optional.empty()).build();
- final String credentialCreationOptions =
- jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
- webAuthnContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
+
+ webAuthnRegContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
final WebAuthnAuthenticationClient client = new YubicoWebauthnAuthenticationClient(rp, jsonMapper);
validator.setWebAuthnClient(client);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list