[java-idp-plugin-webauthn] branch main updated: Update views. Add more Yubico types to client interface
Phil Smart
philip.smart at jisc.ac.uk
Wed Dec 6 14:05:49 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=dcf3cdfbe1940c71f29e08ac2f419bbd213306ed
The following commit(s) were added to refs/heads/main by this push:
new dcf3cdf Update views. Add more Yubico types to client interface
dcf3cdf is described below
commit dcf3cdfbe1940c71f29e08ac2f419bbd213306ed
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Dec 6 14:05:46 2023 +0000
Update views. Add more Yubico types to client interface
- Now works end to end for discoverable keys
---
...Action.java => AbstractWebAuthnBaseAction.java} | 92 +++++---
.../AbstractWebAuthnRegistrationAction.java | 33 ++-
.../webauthn/WebAuthnAuthenticationClient.java | 29 ++-
.../webauthn/context/BaseWebAuthnContext.java | 54 +++++
.../context/WebAuthnAuthenticationContext.java | 38 +---
.../context/WebAuthnRegistrationContext.java | 16 +-
.../CreatePublicKeyCredentialCreationOptions.java | 3 +-
...actAuthenticatorAttestationFromFormRequest.java | 17 +-
.../admin/impl/LookupRegisteredCredentials.java | 63 ++++++
.../admin/impl/StorePublicKeyCredential.java | 41 +---
.../ValidateAuthenticatorAttestationResponse.java | 11 +-
.../impl/YubicoWebauthnAuthenticationClient.java | 69 +++---
...tractAuthenticatorAssertionFromFormRequest.java | 28 ++-
.../webauthn/impl/GenerateServerChallenge.java | 50 +----
.../PopulateWebAuthnAuthenticationContext.java | 2 +-
.../webauthn/impl/ValidateWebAuthnAssertion.java | 16 +-
.../webauthn-registration-beans.xml | 11 +-
.../webauthn-registration-flow.xml | 1 +
.../authn/WebAuthn/webauthn-abstract-beans.xml | 13 +-
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 13 +-
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 2 +
.../webauthn/views/webauthn-auth-old-backup.vm | 241 +++++++++++++++++++++
.../plugin/authn/webauthn/views/webauthn-authn.vm | 28 ++-
.../authn/webauthn/views/webauthn-register.vm | 47 ++--
.../authn/webauthn/views/webauthn-selector.vm | 2 +-
25 files changed, 683 insertions(+), 237 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnBaseAction.java
similarity index 58%
copy from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
copy to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnBaseAction.java
index 017d9e8..a1034d1 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnBaseAction.java
@@ -27,9 +27,12 @@ import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import com.yubico.webauthn.CredentialRepository;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -37,33 +40,38 @@ import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * <p>A base class for WebAuthn registration related admin actions.</p>
+ * <p>A base class for WebAuthn actions that require access to the base WebAuthn context.</p>
*
* <p>In addition to the work performed by {@link AbstractProfileAction}, this action also looks up
- * and makes available the {@link WebAuthnRegistrationContext}.</p>
+ * and makes available the {@link BaseWebAuthnContext}.</p>
*
* <p>WebAuthn registration action implementations should override the
- * {@link #doExecute(ProfileRequestContext, WebAuthnRegistrationContext)} method.</p>
+ * {@link #doExecute(ProfileRequestContext, BaseWebAuthnContext)} method.</p>
*
* @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 AbstractWebAuthnRegistrationAction extends AbstractProfileAction {
+//TODO a few abstract actions, remove overlap
+public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
/** Class logger. */
- @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnRegistrationAction.class);
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnBaseAction.class);
- /** Lookup strategy to locate the webauthn registration context. */
+ /** Lookup strategy to locate the WebAuthn base context. */
@Nonnull
- private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
+ private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnBaseContextLookupStrategy;
- /** The WebAuthn registration Context.*/
- @NonnullBeforeExec private WebAuthnRegistrationContext webauthnRegistrationContext;
+ /** The WebAuthn base Context.*/
+ @NonnullBeforeExec private BaseWebAuthnContext webAuthnBaseContext;
/** The WebAuthn client to use.*/
@NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+ /** The credential repository to store valid credentials in.*/
+ // TODO replace with an adaptor to the storage service?
+ @NonnullAfterInit private CredentialRepository credentialRepository;
+
/**
* Set the WebAuthn client used to handle registration and authentication ceremonies.
@@ -87,25 +95,46 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
/** Constructor.*/
- protected AbstractWebAuthnRegistrationAction() {
- //prc -> WebAuthnContext
- webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+ protected AbstractWebAuthnBaseAction() {
+ //prc -> WebAuthnBaseContext
+ webAuthnBaseContextLookupStrategy = new ChildContextLookup<>(BaseWebAuthnContext.class).
+ compose(new ChildContextLookup<>(AuthenticationContext.class));
}
/**
- * Set WebAuthn registration context lookup strategy to use.
+ * Set WebAuthn base context lookup strategy to use.
*
* @param strategy lookup strategy
*/
- public void setWebauthnRegistrationContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
+ public void setWebAuthnBaseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,BaseWebAuthnContext> strategy) {
checkSetterPreconditions();
- webauthnRegistrationContextLookupStrategy =
+ webAuthnBaseContextLookupStrategy =
Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
}
+
+ /**
+ * Set the credential repository used to store WebAuthn credentials.
+ *
+ * @param repository The respository to set.
+ */
+ public void setCredentialRepository(@Nonnull final CredentialRepository repository) {
+ checkSetterPreconditions();
+ credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+ }
+
+ /**
+ * Get the credential repository used to store WebAuthn credentials.
+ *
+ * @return the credential repository.
+ */
+ public CredentialRepository getCredentialRepository() {
+ return credentialRepository;
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -114,6 +143,9 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
if (webAuthnClient == null) {
throw new ComponentInitializationException("WebAuthn Client can not be null");
}
+ if (credentialRepository == null) {
+ throw new ComponentInitializationException("CredentialRepository can not be null");
+ }
}
/** {@inheritDoc} */
@@ -124,18 +156,18 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
return false;
}
- webauthnRegistrationContext = webauthnRegistrationContextLookupStrategy.apply(profileRequestContext);
- if (webauthnRegistrationContext == null) {
- log.warn("{} No WebAuthn registration context returned by lookup strategy",getLogPrefix());
+ webAuthnBaseContext = webAuthnBaseContextLookupStrategy.apply(profileRequestContext);
+ if (webAuthnBaseContext == null) {
+ log.warn("{} No WebAuthn base context returned by lookup strategy",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
- return doPreExecute(profileRequestContext, webauthnRegistrationContext);
+ return doPreExecute(profileRequestContext, webAuthnBaseContext);
}
/**
- * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnAuthenticationContext)} to perform the
+ * Delegates to {@link #doExecute(ProfileRequestContext, BaseWebAuthnContext)} to perform the
* actual action. Implementations can not override this method.
*
* @param profileRequestContext the current IdP profile request context
@@ -143,31 +175,31 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
@Override
protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- doExecute(profileRequestContext, webauthnRegistrationContext);
+ doExecute(profileRequestContext, webAuthnBaseContext);
}
/**
- * Performs this admin action's pre-execute step. Default implementation just returns true.
+ * Performs this action's pre-execute step. Default implementation just returns true.
*
* @param profileRequestContext the current IdP profile request context
- * @param context the WebAuthn registration context
+ * @param context the WebAuthn base context
*
* @return true iff execution should continue
*/
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final WebAuthnRegistrationContext context) {
+ @Nonnull final BaseWebAuthnContext context) {
return true;
}
/**
- * Performs this webauthn authentication action using the supplied webauthn context. Implementations
+ * Performs this WebAuthn authentication action using the supplied WebAuthn context. Implementations
* should override this method.
*
* @param profileRequestContext the current IdP profile request context
- * @param context the WebAuthn registration context
+ * @param context the WebAuthn base context
*/
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final WebAuthnRegistrationContext context) {
+ @Nonnull final BaseWebAuthnContext context) {
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
index 017d9e8..3eb3f2e 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
@@ -27,9 +27,11 @@ import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import com.yubico.webauthn.CredentialRepository;
+
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -64,6 +66,10 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
/** The WebAuthn client to use.*/
@NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+ /** The credential respository to store valid credentials in.*/
+ // TODO replace with an adaptor to the storage service?
+ @NonnullAfterInit private CredentialRepository credentialRepository;
+
/**
* Set the WebAuthn client used to handle registration and authentication ceremonies.
@@ -106,6 +112,26 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
}
+
+ /**
+ * Set the credential repository used to store webauthn credentials.
+ *
+ * @param repository The respository to set.
+ */
+ public void setCredentialRepository(@Nonnull final CredentialRepository repository) {
+ checkSetterPreconditions();
+ credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+ }
+
+ /**
+ * Get the credential repository used to store webauthn credentials.
+ *
+ * @return the credential repository.
+ */
+ public CredentialRepository getCredentialRepository() {
+ return credentialRepository;
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -114,6 +140,9 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
if (webAuthnClient == null) {
throw new ComponentInitializationException("WebAuthn Client can not be null");
}
+ if (credentialRepository == null) {
+ throw new ComponentInitializationException("CredentialRepository can not be null");
+ }
}
/** {@inheritDoc} */
@@ -135,7 +164,7 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
}
/**
- * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnAuthenticationContext)} to perform the
+ * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnRegistrationContext)} to perform the
* actual action. Implementations can not override this method.
*
* @param profileRequestContext the current IdP profile request context
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 53b820e..d44024d 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
@@ -1,12 +1,20 @@
package net.shibboleth.idp.plugin.authn.webauthn;
+import java.util.Set;
+
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.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
/**
@@ -27,8 +35,9 @@ public interface WebAuthnAuthenticationClient {
* @param username the username of the username that has been pre-identified. Can be null
* if no user has been identified, and the IdP is requesting the client discover
* the credential.
- * @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 allowCredentials the set of public key credentials acceptable to the IdP in descending order of
+ * preference
+ * @param challenge the challenge the authenticator signs along with other data to produce an assertion
*
* @return a PublicKeyCredentialRequestOptions object
*
@@ -36,7 +45,7 @@ public interface WebAuthnAuthenticationClient {
*
*/
@Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username,
- @Nullable final byte[] userHandle, @Nonnull final byte[] challenge)
+ @Nullable final Set<PublicKeyCredentialDescriptor> allowCredentials, @Nonnull final byte[] challenge)
throws WebAuthnAuthenticationClientException;
/**
@@ -53,18 +62,19 @@ public interface WebAuthnAuthenticationClient {
*
* @throws WebAuthnAuthenticationClientException if there is an error generating the creation request
*/
- @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(@Nullable final String username,
+ @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(
+ @Nullable final Set<PublicKeyCredentialDescriptor> excludeCredentials, @Nullable final String username,
@Nullable final byte[] userHandle, @Nonnull final byte[] challenge)
throws WebAuthnAuthenticationClientException;
/**
- * Validate the Authenticator Assertion Response from an authentication request
+ * 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.
+ * @param authenticatorAssertionResponse the assertion response.
*
* @return an assertion result if the assertion was valid.
*
@@ -73,8 +83,8 @@ public interface WebAuthnAuthenticationClient {
AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username,
@Nullable final byte[] userHandle,
@Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
- @Nonnull final String authenticatorAssertionResponse)
- throws AssertionFailureException;
+ @Nonnull final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ authenticatorAssertionResponse) throws AssertionFailureException;
/**
@@ -89,6 +99,7 @@ public interface WebAuthnAuthenticationClient {
*/
RegistrationResult validateAuthenticatorAttestationResponse(
@Nonnull final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions,
- @Nonnull final String authenticatorAttestationResponse) throws RegistrationFailureException;
+ @Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ 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
index 4cf3a25..3632834 100644
--- 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
@@ -14,11 +14,15 @@
package net.shibboleth.idp.plugin.authn.webauthn.context;
+import java.util.Set;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.messaging.context.BaseContext;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.logic.Constraint;
@@ -28,12 +32,62 @@ import net.shibboleth.shared.logic.Constraint;
*/
public class BaseWebAuthnContext extends BaseContext {
+ /** The original username. */
+ @Nullable private String username;
+
+ /**
+ * Credentials that have already been registered with the IdP. The authenticator should use these to avoid creating
+ * duplicate credentials during registration, or to tell the browser which credentials to use during authentication.
+ */
+ @Nullable private Set<PublicKeyCredentialDescriptor> existingCredentials;
+
/** 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;
+ /**
+ * 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 BaseWebAuthnContext setUsername(@Nullable final String name) {
+ username = name;
+ //transformedUsername = name;
+ return this;
+ }
+
+ /**
+ * Set the credentials that have already been registered with the IdP.
+ *
+ * @param credentials the set of credentials
+ */
+ public void setExistingCredentials(@Nullable final Set<PublicKeyCredentialDescriptor> credentials) {
+ existingCredentials = credentials;
+
+ }
+
+ /**
+ * Get the credentials that have already been registered with the IdP.
+ *
+ * @return the excluded credentials.
+ */
+ public Set<PublicKeyCredentialDescriptor> getExistingCredentials() {
+ return existingCredentials;
+ }
+
/**
* Get the server challenge sent to the authenticator.
*
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 7729c59..153a7d5 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,6 +4,9 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import net.shibboleth.shared.logic.Constraint;
@@ -19,11 +22,9 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
/** The credential identifier generated by the authenticator.*/
@Nullable private byte[] credentialId;
- /** The original username. */
- @Nullable private String username;
-
/** An assertion response that is the result of an authentication.*/
- @Nullable private String authenticatorAssertionResponse;
+ @Nullable
+ private PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> authenticatorAssertionResponse;
/** The public key credential request options for authentication.*/
@Nullable private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
@@ -35,28 +36,6 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
/** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
@Nullable private byte[] userHandle;
- /**
- * Gets the username.
- *
- * @return the username
- */
- @Nullable public String getUsername() {
- return username;
- }
-
- /**
- * Sets the username and resets the transformed version to be identical.
- *
- * @param name the username
- *
- * @return this context
- */
- @Nonnull public WebAuthnAuthenticationContext setUsername(@Nullable final String name) {
- username = name;
- //transformedUsername = name;
- return this;
- }
-
/**
* Set the public key, as a byte array, in COSE_Key format.
@@ -113,7 +92,9 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
*
* @param assertion The authenticator assertion response to set.
*/
- public WebAuthnAuthenticationContext setAuthenticatorAssertionResponse(@Nullable final String assertion) {
+ public WebAuthnAuthenticationContext setAuthenticatorAssertionResponse(
+ @Nullable final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion) {
authenticatorAssertionResponse = assertion;
return this;
}
@@ -123,7 +104,8 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
*
* @return the authenticator a ssertion response.
*/
- @Nullable public String getAuthenticatorAssertionResponse() {
+ @Nullable public PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ getAuthenticatorAssertionResponse() {
return authenticatorAssertionResponse;
}
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
index d2b7348..e7d0cda 100644
--- 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
@@ -5,6 +5,9 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
@@ -24,7 +27,8 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
@Nullable private byte[] existingPublicKey;
/** An assertion response that is the result of an authentication.*/
- @Nullable private String authenticatorAttestationResponse;
+ @Nullable private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ authenticatorAttestationResponse;
/** The public key credential creation options for registration.*/
@Nullable private PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions;
@@ -44,7 +48,8 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @return Returns the authenticator attestation response.
*/
- @Nullable public String getAuthenticatorAttestationResponse() {
+ @Nullable public PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ getAuthenticatorAttestationResponse() {
return authenticatorAttestationResponse;
}
@@ -53,7 +58,9 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
*/
- public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(@Nullable final String attestation) {
+ public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(
+ @Nullable final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ attestation) {
authenticatorAttestationResponse = attestation;
return this;
}
@@ -63,6 +70,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @return the username
*/
+ @Override
@Nullable public String getUsername() {
return username;
}
@@ -74,6 +82,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @return this context
*/
+ @Override
@Nonnull public WebAuthnRegistrationContext setUsername(@Nullable final String name) {
username = name;
//transformedUsername = name;
@@ -139,4 +148,5 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
return this;
}
+
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
index 49ac254..e97ba8c 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -83,7 +83,8 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
try {
final PublicKeyCredentialCreationOptions pkCredCreationOptions =
- client.createRegistrationRequest(context.getUsername(), context.getUserHandle(), challenge);
+ client.createRegistrationRequest(context.getExistingCredentials(),context.getUsername(),
+ context.getUserHandle(), challenge);
context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
//convert to JSON
context.setPublicKeyCredentialCreationOptionsJSON(
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
index 30551b9..dae40fe 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
@@ -17,6 +17,8 @@
package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+import java.io.IOException;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -25,6 +27,9 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.authn.AuthnEventIds;
@@ -110,7 +115,17 @@ public class ExtractAuthenticatorAttestationFromFormRequest extends AbstractWebA
return;
}
- context.setAuthenticatorAttestationResponse(pkCredAttestationJson);
+ try {
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ pkCredAttestation = PublicKeyCredential.parseRegistrationResponseJson(pkCredAttestationJson);
+ context.setAuthenticatorAttestationResponse(pkCredAttestation);
+ } catch (final IOException e) {
+ log.warn("{} Could not convert AuthenticatorAttestationResponse from form", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupRegisteredCredentials.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupRegisteredCredentials.java
new file mode 100644
index 0000000..35d8c1a
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupRegisteredCredentials.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnBaseAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that lookups existing registered credentials
+ */
+public class LookupRegisteredCredentials extends AbstractWebAuthnBaseAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(LookupRegisteredCredentials.class);
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final BaseWebAuthnContext context) {
+
+ final String username = context.getUsername();
+
+ if (username == null) {
+ log.error("Unable to find username in registration context");
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return;
+ }
+
+ final Set<PublicKeyCredentialDescriptor> credentials =
+ getCredentialRepository().getCredentialIdsForUsername(username);
+
+ log.debug("{} Found '{}' registered credentials for '{}'", getLogPrefix(),
+ credentials != null ? credentials.size() : "0", username);
+
+ context.setExistingCredentials(credentials);
+
+
+ }
+
+}
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
index 1d5ba1a..2c1c36c 100644
--- 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
@@ -31,7 +31,6 @@ 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;
@@ -50,6 +49,9 @@ import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
+/**
+ * An action that stores the public key credential into the credential repository.
+ */
public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction {
/** Class logger. */
@@ -60,9 +62,6 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
@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
@@ -72,17 +71,6 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
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.
@@ -101,23 +89,12 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
@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) {
@@ -125,35 +102,35 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
final String username = context.getUsername();
if (username == null) {
- log.error("Unable to find username in registration response");
+ log.error("Unable to find username in registration context");
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");
+ log.error("Unable to find registration information in registration context");
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return;
}
try {
final RegisteredCredential credential = RegisteredCredential.builder()
.credentialId(registrationResult.getKeyId().getId())
- .userHandle(ByteArray.fromBase64("bm90YWhhbmRsZQ=="))
+ .userHandle(new ByteArray(context.getUserHandle()))
.publicKeyCose(registrationResult.getPublicKeyCose())
.build();
final UserIdentity user = UserIdentity.builder()
.name(username)
.displayName(username)
- .id(ByteArray.fromBase64("bm90YWhhbmRsZQ=="))
+ .id(new ByteArray(context.getUserHandle()))
.build();
// TODO fixup the record we will use to store registrations
- final CredentialRegistration registration = new CredentialRegistration(user, Optional.of("Nickanme"),
+ final CredentialRegistration registration = new CredentialRegistration(user, Optional.of("Nickname"),
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) {
+ if (getCredentialRepository() 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());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
index 12e3bc2..b6becb0 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
@@ -24,6 +24,9 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
import net.shibboleth.idp.authn.AuthnEventIds;
@@ -33,7 +36,6 @@ import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationCont
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. If valid store it inside the credential repository.
@@ -48,7 +50,8 @@ public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnRe
@NonnullBeforeExec @NotEmpty private PublicKeyCredentialCreationOptions pkCredCreationOptions;
/** The stashed authenticator response.*/
- @NonnullBeforeExec @NotEmpty private String attestation;
+ @NonnullBeforeExec
+ private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation;
/** {@inheritDoc} */
@@ -61,8 +64,8 @@ public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnRe
}
attestation = context.getAuthenticatorAttestationResponse();
- if (StringSupport.trimOrNull(attestation) == null) {
- log.error("{} public key credential was null", getLogPrefix());
+ if (attestation == null) {
+ log.error("{} authenticator attestation response was null", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return false;
}
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 a0b268e..fd2f271 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
@@ -14,11 +14,12 @@
package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
-import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -34,12 +35,17 @@ 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.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialParameters;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
import com.yubico.webauthn.data.UserIdentity;
import com.yubico.webauthn.data.UserVerificationRequirement;
import com.yubico.webauthn.exception.RegistrationFailedException;
@@ -95,7 +101,8 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
@Override
public PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username,
- final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
+ @Nullable final Set<PublicKeyCredentialDescriptor> allowCredentials, final byte[] challenge)
+ throws WebAuthnAuthenticationClientException {
//set default to preferred.
UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
@@ -103,24 +110,15 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
//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());
+
+ // The order of these credentials becomes important
+ final List<PublicKeyCredentialDescriptor> listOfCredentials = allowCredentials == null ? null :
+ new ArrayList<>(allowCredentials);
+
final PublicKeyCredentialRequestOptions request = PublicKeyCredentialRequestOptions.builder()
.challenge(new ByteArray(challenge))
.rpId(rp.getIdentity().getId())
-// .allowCredentials(
-// OptionalUtil.orElseOptional(
-// startAssertionOptions.getUsername(),
-// () ->
-// startAssertionOptions
-// .getUserHandle()
-// .flatMap(credentialRepository::getUsernameForUserHandle))
-// .map(
-// un ->
-// new ArrayList<>(credentialRepository.getCredentialIdsForUsername(un))))
+ .allowCredentials(Optional.ofNullable(listOfCredentials))
// .extensions(
// startAssertionOptions
// .getExtensions()
@@ -137,7 +135,8 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
/** {@inheritDoc} */
@Override
- public PublicKeyCredentialCreationOptions createRegistrationRequest(final String username,
+ public PublicKeyCredentialCreationOptions createRegistrationRequest(
+ @Nullable final Set<PublicKeyCredentialDescriptor> excludeCredentials, final String username,
final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
//set default to preferred.
@@ -146,6 +145,7 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
//then require user verification? makes sense, but is that part of the spec?
userVerificationRequirement = UserVerificationRequirement.REQUIRED;
}
+ final ResidentKeyRequirement residentKeyRquirement = ResidentKeyRequirement.REQUIRED;
final UserIdentity identity =
UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
@@ -154,10 +154,11 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
.user(identity)
.challenge(new ByteArray(challenge))
.pubKeyCredParams(preferredPublickeyParams)
- .excludeCredentials(Optional.empty())
-// .excludeCredentials(
-// credentialRepository.getCredentialIdsForUsername(
-// startRegistrationOptions.getUser().getName()))
+ .excludeCredentials(excludeCredentials)
+ .authenticatorSelection(AuthenticatorSelectionCriteria.builder()
+ .userVerification(userVerificationRequirement)
+ .residentKey(residentKeyRquirement)
+ .build())
// .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection())
// .extensions(
// startRegistrationOptions
@@ -178,7 +179,8 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
public AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username,
@Nullable final byte[] userHandle,
@Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
- @Nonnull final String authenticatorAssertionResponse) throws AssertionFailureException {
+ @Nonnull final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ authenticatorAssertionResponse) throws AssertionFailureException {
try {
@@ -187,14 +189,12 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
.userHandle(Optional.ofNullable(userHandle != null ? new ByteArray(userHandle) : null))
.username(Optional.ofNullable(username))
.build();
-
- final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkCred =
- PublicKeyCredential.parseAssertionResponseJson(authenticatorAssertionResponse);
- log.trace("Client Data '{}'",pkCred.getResponse().getClientData());
- log.trace("Signature '{}'",pkCred.getResponse().getSignature());
+
+ log.trace("Client Data '{}'",authenticatorAssertionResponse.getResponse().getClientData());
+ log.trace("Signature '{}'",authenticatorAssertionResponse.getResponse().getSignature());
final AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(requestAssertion)
- .response(pkCred)
+ .response(authenticatorAssertionResponse)
.build());
if (result == null) {
throw new AssertionFailureException("Unable to validate authenticator assertion");
@@ -215,19 +215,18 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
@Override
public RegistrationResult validateAuthenticatorAttestationResponse(
@Nonnull final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions,
- @Nonnull final String authenticatorAttestationResponse) throws RegistrationFailureException {
+ @Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ authenticatorAttestationResponse) throws RegistrationFailureException {
try {
- log.trace("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
- final var publicKeyRegistration =
- PublicKeyCredential.parseRegistrationResponseJson(authenticatorAttestationResponse);
+ log.trace("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
return rp.finishRegistration(FinishRegistrationOptions.builder()
.request(publicKeyCredentialCreationOptions)
- .response(publicKeyRegistration)
+ .response(authenticatorAttestationResponse)
.build());
- } catch (final RegistrationFailedException | IOException e) {
+ } catch (final RegistrationFailedException e) {
throw new RegistrationFailureException(e);
}
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
index d9ce6e1..ffa2529 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
@@ -17,6 +17,8 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
+import java.io.IOException;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -25,6 +27,9 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.authn.AuthnEventIds;
@@ -102,16 +107,25 @@ public class ExtractAuthenticatorAssertionFromFormRequest extends AbstractWebAut
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return;
}
- // TODO correct object type
- final String pkCredJson = extractAuthenticatorAssertionResponse(request);
- if (pkCredJson == null) {
- log.debug("Public key assertion not found in HTTP request: '{}'",pkCredJson);
+
+ final String pkCredAssertionJson = extractAuthenticatorAssertionResponse(request);
+ if (pkCredAssertionJson == null) {
+ log.debug("{} Public key assertion not found in HTTP request",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return;
}
- log.trace("Public key assertion found: '{}'",pkCredJson);
-
- context.setAuthenticatorAssertionResponse(pkCredJson);
+ log.trace("{} Authenticator assertion response: '{}'",getLogPrefix(),pkCredAssertionJson);
+
+ try {
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion =
+ PublicKeyCredential.parseAssertionResponseJson(pkCredAssertionJson);
+ context.setAuthenticatorAssertionResponse(assertion);
+ } catch (final IOException e) {
+ log.debug("{} Authenticator assertion could not be converted to the correct type",getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
}
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 a6b8490..1bf358c 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,21 +19,16 @@ 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.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnBaseAction;
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;
/**
@@ -45,51 +40,14 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
* @post See above.
*/
-public class GenerateServerChallenge extends AbstractProfileAction {
+public class GenerateServerChallenge extends AbstractWebAuthnBaseAction {
/** 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) {
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final BaseWebAuthnContext context) {
try {
final byte[] challenge = generateChallenge();
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
index 9849f6d..79bb7e3 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
@@ -118,7 +118,7 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
- context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
+ context.setUsername(username);
log.debug("Created Webauthn authentication context");
}
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 293eb15..d1c5061 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
@@ -11,6 +11,9 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import net.shibboleth.idp.authn.AbstractValidationAction;
@@ -103,11 +106,13 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- final String assertion = context.getAuthenticatorAssertionResponse();
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion =
+ context.getAuthenticatorAssertionResponse();
+
+ // TODO these need to be set from lookup earlier in the context in certain types of flows e.g. non-discoverable
+ // Null username and userhandle describes a request for a discoverable credential
+ context.setUsername(null);
- // 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 ",
@@ -118,9 +123,12 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
return;
}
try {
+
final AssertionResult result = webAuthnClient.validateAuthenticatorAssertionResponse(
context.getUsername(), context.getUserHandle(), publicKeyCredentialRequestOptions, assertion);
+ log.info("{} WebAuthn authentication succeeded for '{}'",getLogPrefix(),result.getUsername());
+ context.setUsername(result.getUsername());
buildAuthenticationResult(profileRequestContext, authenticationContext);
} catch (final AssertionFailureException e) {
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 2815d08..da08c57 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
@@ -24,16 +24,21 @@
<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="LookupRegisteredCredentials" parent="AbstractWebAuthnRegistrationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.LookupRegisteredCredentials"
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
- <bean id="GenerateServerChallenge" parent="AbstractGenerateServerChallenge"
+ <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
- <bean id="GenerateUserHandle" parent="AbstractWebAuthnAuthenticationAction"
+ <bean id="GenerateUserHandle" parent="AbstractWebAuthnRegistrationAction"
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"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions"
p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper"/>
<bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
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 ecbef86..983a7cf 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
@@ -25,6 +25,7 @@
<action-state id="PopulateWebAuthnContext">
<evaluate expression="PopulateWebAuthnRegistrationContext"/>
+ <evaluate expression="LookupRegisteredCredentials"/>
<evaluate expression="GenerateServerChallenge"/>
<evaluate expression="GenerateUserHandle"/>
<evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
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 8e6b455..9d7be7d 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
@@ -14,14 +14,13 @@
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="AbstractGenerateServerChallenge" scope="prototype" abstract="true"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge" />
-
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"
+ p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"/>
-
+ <bean id="AbstractWebAuthnBaseAction" scope="prototype" abstract="true"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"
+ p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"/>
+
</beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 9c72842..6f58664 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
@@ -18,12 +18,13 @@
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 ="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+ parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
+ c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
+
+ <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"/>
<bean id="CreatePublicKeyCredentialRequestOptions" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"
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 dc26b5c..0eac3c0 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
@@ -10,6 +10,8 @@
<transition on="proceed" to="DisplayWebAuthnView" />
</action-state>
+ <!-- If passwordless and not usernameless we need a different flow here -->
+
<view-state id="DisplayWebAuthnView" view="webauthn/webauthn-authn">
<on-render>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-auth-old-backup.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-auth-old-backup.vm
new file mode 100644
index 0000000..81212ad
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-auth-old-backup.vm
@@ -0,0 +1,241 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/main.css">
+
+
+ </head>
+
+ <!-- FIXME Externalise this-->
+ <script>
+
+
+ const base64abc = [
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
+ ];
+
+
+ const base64codes = [
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255,
+ 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
+ 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
+ ];
+
+ function bytesToBase64(bytes) {
+ let result = '', i, l = bytes.length;
+ for (i = 2; i < l; i += 3) {
+ result += base64abc[bytes[i - 2] >> 2];
+ result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+ result += base64abc[((bytes[i - 1] & 0x0F) << 2) | (bytes[i] >> 6)];
+ result += base64abc[bytes[i] & 0x3F];
+ }
+ if (i === l + 1) { // 1 octet yet to write
+ result += base64abc[bytes[i - 2] >> 2];
+ result += base64abc[(bytes[i - 2] & 0x03) << 4];
+ result += "==";
+ }
+ if (i === l) { // 2 octets yet to write
+ result += base64abc[bytes[i - 2] >> 2];
+ result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+ result += base64abc[(bytes[i - 1] & 0x0F) << 2];
+ result += "=";
+ }
+ return result;
+ }
+
+ function responseToObject(response) {
+ if (response.u2fResponse) {
+ return response;
+ } else {
+ let clientExtensionResults = {};
+
+ try {
+ clientExtensionResults = response.getClientExtensionResults();
+ } catch (e) {
+ console.error('getClientExtensionResults failed', e);
+ }
+
+ if (response.response.attestationObject) {
+ return {
+ type: response.type,
+ id: response.id,
+ response: {
+ attestationObject: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.attestationObject))),
+ clientDataJSON: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.clientDataJSON))),
+ },
+ clientExtensionResults,
+ };
+ } else {
+ return {
+ type: response.type,
+ id: response.id,
+ response: {
+ authenticatorData: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.authenticatorData))),
+ clientDataJSON: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.clientDataJSON))),
+ signature: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.signature))),
+ userHandle: response.response.userHandle && Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.userHandle))),
+ },
+ clientExtensionResults,
+ };
+ }
+ }
+ }
+
+ if (!window.PublicKeyCredential){
+ console.log("Bad client, died");
+ }
+
+ function ensureUint8Array(arg) {
+ if (arg instanceof ArrayBuffer) {
+ return new Uint8Array(arg);
+ } else {
+ return arg;
+ }
+ }
+ /**
+ * use this to make a Base64 encoded string URL friendly,
+ * i.e. '+' and '/' are replaced with '-' and '_' also any trailing '='
+ * characters are removed
+ *
+ * @param {String} str the encoded string
+ * @returns {String} the URL friendly encoded String
+ */
+ function Base64EncodeUrl(str){
+ return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
+ }
+
+ const publicKeyCredentialCreationOptions = {
+ challenge: Uint8Array.from(
+ "$webauthnContext.serverChallengeBase64", c => c.charCodeAt(0)),
+ rp: {
+ name: "Shib",
+ id: "localhost",
+ },
+ user: {
+ id: Uint8Array.from(
+ "UZSL85T9AFC", c => c.charCodeAt(0)),
+ name: "phil1001 at jisc.ac.uk",
+ displayName: "Phil",
+ },
+ pubKeyCredParams: [{alg: -7, type: "public-key"}],
+ authenticatorSelection: {
+ authenticatorAttachment: "cross-platform",
+ userVerification: "preferred"
+ },
+ timeout: 60000,
+ attestation: "direct"
+ };
+
+
+ async function register() {
+ const credential = await navigator.credentials.create({
+ publicKey: publicKeyCredentialCreationOptions
+ }).catch(console.error);
+
+ console.log("Credential: "+credential);
+ console.log(responseToObject(credential));
+ console.log(JSON.stringify(credential.response));
+ try{
+ document.getElementById("publicKeyCredential").value = JSON.stringify(responseToObject(credential));
+
+ } catch (err) {
+ console.log(err);
+ }
+
+ }
+
+ async function authenticate() {
+ try{
+ console.log("authenticating");
+ const assertion = await navigator.credentials.get({
+ publicKey: {
+ challenge: Uint8Array.from(
+ "$webauthnContext.serverChallengeBase64", c => c.charCodeAt(0)),
+ allowCredentials: [{
+ id: new Uint8Array($webauthnContext.existingCredentialId),
+ type: 'public-key',
+ transports: ['usb', 'ble', 'nfc'],
+ }],
+ timeout: 60000,
+ }
+ });
+ console.log(assertion);
+ document.getElementById("publicKeyAssertion").value = JSON.stringify(responseToObject(assertion));
+ document.getElementById("authn-submit").click();
+ } catch (err) {
+ console.log(err);
+ }
+
+
+
+ }
+
+ </script>
+
+ <body>
+ <div class="wrapper">
+ <div class="container">
+ <header>
+ <img src="$request.getContextPath()#springMessage("idp.logo")" alt="#springMessageText("idp.logo.alt-text", "logo")">
+ </header>
+
+ <div class="content">
+ <div class="column one">
+
+
+
+ <button class="form-element form-button" onclick="authenticate();">Authenticate</button>
+ <form id="authn-form" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <textarea id="publicKeyAssertion" name="publicKeyAssertion" rows="10" cols="50">
+ </textarea>
+ <button id="authn-submit" class="form-element form-button" type="submit" name="_eventId_authenticate">Submit Authenticate</button>
+ </form>
+
+
+ </div>
+
+ </div>
+
+ <footer>
+ <div class="container container-footer">
+ <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </div>
+
+ </body>
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
index 1e98647..fce3aaa 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -28,7 +28,29 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText(" idp.css", "/css/placeholder.css" )">
- <script src="$request.getContextPath()/js/webauthn-shib.js"></script>
+
+ <script type="module">
+ import * as webauthnJson from "$request.getContextPath()/js/webauthn-json.js";
+
+ async function authenticate() {
+ console.log("authenticating");
+ var pkCredRequestOptions = $webauthnContext.publicKeyCredentialRequestOptionsJSON;
+ console.log("Raw request options", pkCredRequestOptions);
+ await webauthnJson.get({ publicKey: pkCredRequestOptions })
+ .then(function (assertion){
+ document.getElementById("publicKeyAssertion").value = JSON.stringify(assertion);
+ }).catch(function (err){console.error});
+
+ }
+
+ function init() {
+ document.getElementById("authenticate").onclick = authenticate;
+ return false;
+ }
+
+ window.onload = init;
+
+</script>
</head>
<body>
@@ -46,7 +68,9 @@
<div class="content">
<div class="column one">
- <button class="form-element form-button" onclick="authenticate();">Authenticate</button>
+ <textarea id="publicKeyCredentialCreation" name="publicKeyCredentialCreation" rows="20" cols="50">
+ $webauthnContext.publicKeyCredentialRequestOptions</textarea>
+ <button id="authenticate" class="form-element form-button">Authenticate</button>
<form id="authn-form" action="$flowExecutionUrl" method="post">
#parse("csrf/csrf.vm")
<textarea id="publicKeyAssertion" name="publicKeyAssertion" rows="10" cols="50">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
index 7703ce9..a20ddba 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -8,7 +8,7 @@
## profileRequestContext - root of context tree
## authenticationContext - context with authentication request information
## authenticationErrorContext - context with login error state
-## webauthnContext = web authentication context
+## webauthnRegContext = web registration context
## authenticationWarningContext - context with login warning state
## rpUIContext - the context with SP UI information from the metadata
## encoder - HTMLEncoder class
@@ -34,8 +34,8 @@
async function register() {
console.log("Register");
- var pkCredOptions = $webauthnContext.publicKeyCredentialCreationOptions;
- console.log("Raw request options", pkCredOptions);
+ var pkCredOptions = $webauthnRegContext.publicKeyCredentialCreationOptionsJSON;
+ console.log("Raw creation options", pkCredOptions);
const credentialJson = await webauthnJson.create({ publicKey: pkCredOptions }).catch(console.error);
console.log("Credential: ", JSON.stringify(credentialJson));
try {
@@ -72,18 +72,35 @@
<section>
<div class="content">
- <div class="column one">
-
- <button class="form-element form-button" id="registerButton">Register</button>
- <textarea id="publicKeyCredentialCreation" name="publicKeyCredentialCreation" rows="20" cols="50">
- $webauthnContext.publicKeyCredentialCreationOptions</textarea>
-
- <form action="$flowExecutionUrl" method="post">
- #parse("csrf/csrf.vm")
- <textarea id="publicKeyCredential" name="publicKeyCredential" rows="10" cols="50"></textarea>
- <button id="reg-submit" type="submit" name="_eventId_proceed">Submit Registration</button>
- </form>
-
+ <div class="column one">
+ <div>
+ <h1>Registered Credentials</h1>
+ #if ($webauthnRegContext.existingCredentials)
+ <ul>
+ #foreach($cred in $webauthnRegContext.existingCredentials)
+ <li>$cred</li>
+ #end
+ </ul>
+ #else
+ <span>You have no registered credentials</span>
+ #end
+ <div>
+ <h1>Registered A New Credential</h1>
+
+
+ <label for="publicKeyCredentialCreation">Registration Options</label>
+ <textarea id="publicKeyCredentialCreation" name="publicKeyCredentialCreation" rows="20" cols="50">
+ $webauthnRegContext.publicKeyCredentialCreationOptions</textarea>
+
+ <button class="form-element form-button" id="registerButton">Register</button>
+
+ <label for="publicKeyCredentialCreation">Registration Response (Attestation)</label>
+ <form action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <textarea id="publicKeyCredential" name="authenticatorAttestation" rows="10" cols="50"></textarea>
+ <button id="reg-submit" type="submit" name="_eventId_proceed">Submit Registration</button>
+ </form>
+ </div>
</div>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
index cf78851..2fc47e6 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
@@ -17,7 +17,7 @@
<!DOCTYPE html>
<html>
<head>
- <title>#springMessageText("idp.title", "Web Login Service")</title>>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list