[java-idp-plugin-webauthn] branch main updated: Generalise interfaces. Improve tests and test mocks
Phil Smart
philip.smart at jisc.ac.uk
Thu Nov 23 17:40:33 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=d45bad9970c28bf061be253cdbbf73be74dc6682
The following commit(s) were added to refs/heads/main by this push:
new d45bad9 Generalise interfaces. Improve tests and test mocks
d45bad9 is described below
commit d45bad9970c28bf061be253cdbbf73be74dc6682
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Nov 23 17:40:25 2023 +0000
Generalise interfaces. Improve tests and test mocks
---
.../webauthn/WebAuthnAuthenticationClient.java | 46 +-
... => WebAuthnAuthenticationClientException.java} | 10 +-
.../WebauthnAuthenticationClientFactory.java | 4 +-
.../context/WebAuthnAuthenticationContext.java | 90 ++--
.../impl/YubicoWebauthnAuthenticationClient.java | 113 +++--
.../CreatePublicKeyCredentialCreationOptions.java | 21 +-
.../CreatePublicKeyCredentialRequestOptions.java | 28 +-
.../ExtractPublicKeyAssertionFromFormRequest.java | 29 +-
.../ExtractPublicKeyCredentialFromFormRequest.java | 37 +-
... ValidateAuthenticatorAttestationResponse.java} | 30 +-
.../webauthn/impl/ValidateWebAuthnAssertion.java | 76 +++-
.../storage/impl/CredentialRegistration.java | 21 +-
.../webauthn-registration-beans.xml | 4 +-
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 15 +-
.../YubicoWebauthnAuthenticationClientTest.java | 290 +++++++++++--
.../authn/webauthn/impl/AbstractWebAuthnTest.java | 30 +-
.../authn/webauthn/impl/MockAuthenticator.java | 480 +++++++++++++++++----
.../impl/ValidatePublicKeyCredentialTest.java | 34 +-
18 files changed, 992 insertions(+), 366 deletions(-)
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 f4e5ff1..bbcbc5d 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,9 @@
package net.shibboleth.idp.plugin.authn.webauthn;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
/**
* A client that manages the entire webauthn authentication and registration ceremony.
*
@@ -23,11 +20,17 @@ 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.
- * @return a JSON serialized PublicKeyCredentialRequestOptions object. Can be null if
- * one could not be created.
+ * @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.
+ *
+ * @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?
- @Nullable String createAuthenticationRequest(@Nullable final String username);
+ @Nonnull String createAuthenticationRequest(@Nullable final String username, @Nullable final byte[] userHandle,
+ @Nonnull final byte[] challenge) throws WebAuthnAuthenticationClientException;
/**
* Create a JSON serialized PublicKeyCredentialCreationOptions.
@@ -40,30 +43,41 @@ public interface WebAuthnAuthenticationClient {
*
* @return a JSON serialized PublicKeyCredentialCreationOptions object. Can be {@code null} if
* one could not be created.
+ *
+ * @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, final byte[] userHandle, final byte[] challenge);
+ @Nullable String createRegistrationRequest(@Nullable final String username, @Nullable final byte[] userHandle,
+ @Nonnull final byte[] challenge) throws WebAuthnAuthenticationClientException;
/**
* Validate the Authenticator Assertion Response.
*
- * @param jsonAssertionResponse the JSON representation of the assertion response.
+ * @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.
+ *
+ * @throws WebAuthnAuthenticationClientException if there is an error during validation
*/
- boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse);
+ boolean validateAuthenticatorAssertionResponse(@Nullable final String username,
+ @Nullable final byte[] userHandle, @Nonnull final String publicKeyCredentialRequestOptions,
+ @Nonnull final String authenticatorAssertionResponse)
+ throws WebAuthnAuthenticationClientException;
/**
* Validate a registration request.
*
- * @param request
- * @param response
- * @return
+ * @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.
*/
- //TODO use other validation method?
- boolean validateRegistration(String request,
- PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> response);
+ boolean validateRegistration(@Nonnull final String publicKeyCredentialCreationOptions,
+ @Nonnull final String authenticatorAttestationResponse);
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClientException.java
similarity index 73%
rename from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java
rename to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClientException.java
index 7e79120..0a876a5 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClientException.java
@@ -3,7 +3,7 @@ package net.shibboleth.idp.plugin.authn.webauthn;
/**
* An exception to signal an error condition during execution or creation of a Webauthn client.
*/
-public class WebauthnAuthenticationClientException extends Exception {
+public class WebAuthnAuthenticationClientException extends Exception {
/** Serial UID. */
private static final long serialVersionUID = -2380145079984333546L;
@@ -12,7 +12,7 @@ public class WebauthnAuthenticationClientException extends Exception {
* Constructor.
*
*/
- public WebauthnAuthenticationClientException() {
+ public WebAuthnAuthenticationClientException() {
super();
}
@@ -23,7 +23,7 @@ public class WebauthnAuthenticationClientException extends Exception {
* @param message exception message
* @param cause exception to be wrapped by this one
*/
- public WebauthnAuthenticationClientException(final String message, final Throwable cause) {
+ public WebAuthnAuthenticationClientException(final String message, final Throwable cause) {
super(message, cause);
}
@@ -33,7 +33,7 @@ public class WebauthnAuthenticationClientException extends Exception {
*
* @param message exception message
*/
- public WebauthnAuthenticationClientException(final String message) {
+ public WebAuthnAuthenticationClientException(final String message) {
super(message);
}
@@ -43,7 +43,7 @@ public class WebauthnAuthenticationClientException extends Exception {
*
* @param cause exception to be wrapped by this one
*/
- public WebauthnAuthenticationClientException(final Throwable cause) {
+ public WebAuthnAuthenticationClientException(final Throwable cause) {
super(cause);
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
index 4968181..b0193b6 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
@@ -13,8 +13,8 @@ public interface WebauthnAuthenticationClientFactory {
*
* @return the client, never {@code nul}.
*
- * @throws WebauthnAuthenticationClientException if there is an errtor creating the client.
+ * @throws WebAuthnAuthenticationClientException if there is an errtor creating the client.
*/
- @Nonnull WebAuthnAuthenticationClient createInstance() throws WebauthnAuthenticationClientException;
+ @Nonnull WebAuthnAuthenticationClient createInstance() throws WebAuthnAuthenticationClientException;
}
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 9eb0209..83a9f9b 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
@@ -6,11 +6,6 @@ import javax.annotation.concurrent.NotThreadSafe;
import org.opensaml.messaging.context.BaseContext;
-import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.logic.Constraint;
@@ -21,11 +16,7 @@ import net.shibboleth.shared.logic.Constraint;
public final class WebAuthnAuthenticationContext extends BaseContext {
@Nullable private byte[] serverChallenge;
-
- /** A registration public key credential.*/
- @Nullable
- private PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> publicKeyCredential;
-
+
/** The credential public key encoded in COSE_Key format.*/
@Nullable private byte[] publicKey;
@@ -39,16 +30,19 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
@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;
+ @Nullable private byte[] existingPublicKey;
+
+ /** An assertion response that is the result of an authentication.*/
+ @Nullable private String authenticatorAssertionResponse;
- //TODO would need to be bytes if Yubico agnostic (if we wanted that)
/** An assertion response that is the result of an authentication.*/
- @Nullable
- private PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs> assertionResponse;
+ @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;
/**
* Set the server challenge which the client authenticator needs to sign.
@@ -63,21 +57,23 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
serverChallenge = challenge;
return this;
}
-
+
/**
- * Get the assertion response as a result of authentication.
+ * Get the attestation response as a result of creating a new credential.
*
- * @return the assertion.
+ * @return Returns the authenticator attestation response.
*/
- @Nullable public PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs>
- getAssertionResponse() {
- return assertionResponse;
+ @Nullable public String getAuthenticatorAttestationResponse() {
+ return authenticatorAttestationResponse;
}
- @Nonnull public WebAuthnAuthenticationContext setAssertionResponse(@Nullable final
- PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs> assertion) {
- assertionResponse = assertion;
- return this;
+ /**
+ * 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;
}
/**
@@ -187,17 +183,6 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
return this;
}
- @Nullable
- public PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> getPublicKeyCredential() {
- return publicKeyCredential;
- }
-
- // From creation of a credential
- public void setPublicKeyCredential(
- @Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential) {
- publicKeyCredential = Constraint.isNotNull(credential, "Public Key Credential can not be null or empty");
- }
-
//TODO throw in a context, this could be null (which is bad here?)
@SuppressWarnings("null")
@Nullable public String getServerChallengeBase64() throws EncodingException {
@@ -222,5 +207,40 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
@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) {
+ publicKeyCredentialRequestOptions = options;
+ }
+
+ /**
+ * Get the public key credential request options.
+ *
+ * @return the public key credential request options.
+ */
+ @Nullable public String getPublicKeyCredentialRequestOptions() {
+ return publicKeyCredentialRequestOptions;
+ }
+ /**
+ * Set the raw authenticator assertion response, returned after the authentication ceremony.
+ *
+ * @param assertion The authenticator assertion response to set.
+ */
+ public void setAuthenticatorAssertionResponse(@Nullable final String assertion) {
+ authenticatorAssertionResponse = assertion;
+ }
+
+ /**
+ * Get the authenticator assertion response, returned after the authentication ceremony.
+ *
+ * @return the authenticator a ssertion response.
+ */
+ @Nullable public String getAuthenticatorAssertionResponse() {
+ return authenticatorAssertionResponse;
+ }
}
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 beaa84c..f78bad6 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,6 +14,7 @@
package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
+import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -28,25 +29,28 @@ 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.FinishAssertionOptions;
import com.yubico.webauthn.FinishRegistrationOptions;
import com.yubico.webauthn.RelyingParty;
-import com.yubico.webauthn.StartAssertionOptions;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
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.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * Yuibico version of the {@link WebAuthnAuthenticationClient}.
+ * Yuibico implementation of a {@link WebAuthnAuthenticationClient}.
*
* <p>Thread-safe, only a single instance is required.</p>
*/
@@ -88,7 +92,8 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
}
@Override
- public String createAuthenticationRequest(@Nullable final String username) {
+ public String createAuthenticationRequest(@Nullable final String username, final byte[] userHandle,
+ final byte[] challenge) throws WebAuthnAuthenticationClientException {
try {
//set default to preferred.
UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
@@ -96,22 +101,44 @@ 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());
- return assertion.toJson();
+// 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())
+// .allowCredentials(
+// OptionalUtil.orElseOptional(
+// startAssertionOptions.getUsername(),
+// () ->
+// startAssertionOptions
+// .getUserHandle()
+// .flatMap(credentialRepository::getUsernameForUserHandle))
+// .map(
+// un ->
+// new ArrayList<>(credentialRepository.getCredentialIdsForUsername(un))))
+// .extensions(
+// startAssertionOptions
+// .getExtensions()
+// .merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
+ .userVerification(userVerificationRequirement)
+ .timeout(Optional.of(60000l))
+ .build();
+
+ return om.writerWithDefaultPrettyPrinter().writeValueAsString(pkcro);
}
catch (final JsonProcessingException e) {
- log.error("Could not construct FIDO2 authentication request for '{}'",username,e);
+ throw new WebAuthnAuthenticationClientException(e);
}
- return null;
}
/** {@inheritDoc} */
@Override
- public String createRegistrationRequest(final String username, final byte[] userHandle, final byte[] challenge) {
+ public String createRegistrationRequest(final String username, final byte[] userHandle, final byte[] challenge)
+ throws WebAuthnAuthenticationClientException {
try {
//set default to preferred.
UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
@@ -145,35 +172,68 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
return om.writerWithDefaultPrettyPrinter().writeValueAsString(options);
}
catch (final JsonProcessingException e) {
- log.error("Could not construct FIDO2 authentication request for '{}'",username,e);
+ throw new WebAuthnAuthenticationClientException(e);
}
- return null;
}
@Override
- public boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse) {
-
- if (jsonAssertionResponse == null || jsonAssertionResponse.isEmpty()) {
- log.warn("JSON Assertion Response is either null or empty, authentication can not be validated");
+ public boolean validateAuthenticatorAssertionResponse(@Nullable final String username,
+ @Nullable final byte[] userHandle,
+ @Nonnull final String publicKeyCredentialRequestOptions,
+ @Nonnull final String authenticatorAssertionResponse) throws WebAuthnAuthenticationClientException {
+
+ try {
+
+ final PublicKeyCredentialRequestOptions request =
+ om.readValue(publicKeyCredentialRequestOptions, PublicKeyCredentialRequestOptions.class);
+
+ final AssertionRequest requestAssertion = AssertionRequest.builder()
+ .publicKeyCredentialRequestOptions(request)
+ // TODO these
+ .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());
+ rp.finishAssertion(FinishAssertionOptions.builder()
+ .request(requestAssertion)
+ .response(pkCred)
+ .build());
+
+ } 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;
}
/** {@inheritDoc} */
@Override
- public boolean validateRegistration(final String request,
- final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> response) {
+ public boolean validateRegistration(final String publicKeyCredentialCreationOptions,
+ final String authenticatorAttestationResponse) {
try {
final PublicKeyCredentialCreationOptions requestOptions =
- PublicKeyCredentialCreationOptions.fromJson(request);
+ PublicKeyCredentialCreationOptions.fromJson(publicKeyCredentialCreationOptions);
+
+ log.debug("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
+ final var publicKeyRegistration =
+ PublicKeyCredential.parseRegistrationResponseJson(authenticatorAttestationResponse);
+
+
rp.finishRegistration(FinishRegistrationOptions.builder()
.request(requestOptions)
- .response(response)
+ .response(publicKeyRegistration)
.build());
- } catch (final RegistrationFailedException | JsonProcessingException e) {
- log.error("Public key credential can not be registered", e);
+ // 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;
}
@@ -182,4 +242,5 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
+
}
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/impl/CreatePublicKeyCredentialCreationOptions.java
index 8bec478..df58513 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/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -28,6 +28,7 @@ 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.primitive.LoggerFactory;
@@ -45,20 +46,20 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAu
@Nonnull final WebAuthnAuthenticationContext context) {
final WebAuthnAuthenticationClient client = getWebAuthnClient();
- if (client == null) {
- log.error("{} WebAuthn client is null, has the context been created correctly?",getLogPrefix());
+ final byte[] challenge = context.getServerChallenge();
+ if (challenge == null) {
+ log.error("{} WebAuthn challenge is null, has the context been created correctly?",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
}
try {
- final String pkCredCreationOptions =
- client.createRegistrationRequest(context.getUsername(), generateUserHandle(),
- context.getServerChallenge());
- //verify correct JSON response?
- context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
- log.debug("Created PublicKeyCredentialCreationOptions '{}'",pkCredCreationOptions);
- } catch (final NoSuchAlgorithmException e) {
+ final String pkCredCreationOptions =
+ client.createRegistrationRequest(context.getUsername(), generateUserHandle(), challenge);
+ //verify correct JSON response?
+ context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
+ log.debug("{} Created PublicKeyCredentialCreationOptions '{}'",getLogPrefix(), pkCredCreationOptions);
+ } catch (final NoSuchAlgorithmException | WebAuthnAuthenticationClientException e) {
log.error("{} Unable to generate PublicKeyCredentialCreationOptions",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
@@ -76,7 +77,7 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAu
@Nonnull private byte[] generateUserHandle() throws NoSuchAlgorithmException {
final byte[] bytes = new byte[32];
SecureRandom.getInstanceStrong().nextBytes(bytes);
- log.trace("Generated '{}' byte challenge",bytes.length);
+ 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/CreatePublicKeyCredentialRequestOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
index 1068f35..12ed08e 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
@@ -25,6 +25,7 @@ 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.primitive.LoggerFactory;
@@ -41,18 +42,29 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
@Nonnull final AuthenticationContext authenticationContext,
@Nonnull final WebAuthnAuthenticationContext context) {
- final WebAuthnAuthenticationClient client = getWebAuthnClient();
- if (client == null) {
- log.error("{} WebAuthn client is null, has the context been created correctly?",getLogPrefix());
+ final WebAuthnAuthenticationClient client = getWebAuthnClient();
+
+ final byte[] challenge = context.getServerChallenge();
+ if (challenge == null) {
+ log.error("{} WebAuthn challenge is null, has the context been created correctly?",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
}
- final String pkCredRequestOptions = client.createAuthenticationRequest(context.getUsername());
- //verify correct JSON response?
-
-
- log.debug("Created PublicKeyCredentialRequestOptions '{}'",pkCredRequestOptions);
+ try {
+ //TODO userhandle needs to be pulled out.
+ final String pkCredRequestOptions = client.createAuthenticationRequest(context.getUsername(),
+ null, challenge);
+ //verify correct JSON response?
+ context.setPublicKeyCredentialRequestOptions(pkCredRequestOptions);
+
+ log.debug("{} Created PublicKeyCredentialRequestOptions: '{}'",getLogPrefix(), pkCredRequestOptions);
+ } catch (final WebAuthnAuthenticationClientException 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/ExtractPublicKeyAssertionFromFormRequest.java
index 859e2b3..e55c766 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/ExtractPublicKeyAssertionFromFormRequest.java
@@ -24,12 +24,7 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.authn.AuthnEventIds;
@@ -45,7 +40,7 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
- * An action that derives the PublicKeyCredential from a form parameter.
+ * An action that derives the PublicKeyAssertion from a form parameter.
*/
public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAuthenticationAction {
@@ -108,30 +103,18 @@ public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAu
return;
}
- final String pkCredJson = extractPublicKeyCredential(request);
+ final String pkCredJson = extractAuthenticatorResponse(request);
log.trace("Public Key Assertion in JSON is '{}'",pkCredJson);
if (pkCredJson == null) {
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return;
- }
-
- try {
- final PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs> pkCred =
- objectMapper.readValue(pkCredJson,
- new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs>>() {});
- log.debug("Client Data '{}'",pkCred.getResponse().getClientData());
- log.debug("Signature '{}'",pkCred.getResponse().getSignature());
- context.setAssertionResponse(pkCred);
- } catch (final JsonProcessingException e) {
- log.debug("{} Can not extract public key credential response", getLogPrefix(),e);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
+ }
+ context.setAuthenticatorAssertionResponse(pkCredJson);
+
}
- @Nullable private String extractPublicKeyCredential(@Nonnull final HttpServletRequest httpRequest) {
+ @Nullable private String extractAuthenticatorResponse(@Nonnull final HttpServletRequest httpRequest) {
return httpRequest.getParameter(fieldName);
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
index f1211f4..2d893da 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/impl/ExtractPublicKeyCredentialFromFormRequest.java
@@ -24,12 +24,7 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.authn.AuthnEventIds;
@@ -109,29 +104,25 @@ public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnA
return;
}
- final String pkCredJson = extractPublicKeyCredential(request);
- log.trace("Public Key Credential in JSON is '{}'",pkCredJson);
- if (StringSupport.trimOrNull(pkCredJson) == null) {
+ final String pkCredAttestationJson = extractPublicKeyCredential(request);
+ log.trace("Public Key Credential AuthenticatorAttestationResponse in JSON is '{}'",pkCredAttestationJson);
+ if (StringSupport.trimOrNull(pkCredAttestationJson) == null) {
+ log.warn("{} Could not extract AuthenticatorAttestationResponse from form", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return;
}
-
- try {
- final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> pkCred =
- objectMapper.readValue(pkCredJson,
- new TypeReference<PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs>>() {});
- log.debug("Client Data '{}'",pkCred.getResponse().getClientData());
- log.debug("Attestation Data '{}'",pkCred.getResponse().getAttestation());
- context.setPublicKeyCredential(pkCred);
- } catch (final JsonProcessingException e) {
- log.debug("{} Can not extract public key credential response", getLogPrefix(),e);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
-
+
+ context.setAuthenticatorAttestationResponse(pkCredAttestationJson);
+
}
+ /**
+ * Extract the public key credential from the AuthenticatorAttestationResponse in the form.
+ *
+ * @param httpRequest the http request
+ *
+ * @return the AuthenticationAttestationResponse JSON.
+ */
@Nullable private String extractPublicKeyCredential(@Nonnull final HttpServletRequest httpRequest) {
return httpRequest.getParameter(fieldName);
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
similarity index 75%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
index 0b7258e..cee6f8f 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
@@ -23,11 +23,6 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
@@ -40,18 +35,17 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
* Validate the public key registration attempt.
*/
-public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationAction {
+public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAuthenticationAction {
/** Class logger. */
@Nonnull
- private final Logger log = LoggerFactory.getLogger(ValidatePublicKeyCredential.class);
+ 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;
/** The stashed authenticator response.*/
- @NonnullBeforeExec
- private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkCred;
+ @NonnullBeforeExec @NotEmpty private String attestation;
/** {@inheritDoc} */
@@ -60,8 +54,8 @@ public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationA
@Nonnull final AuthenticationContext authenticationContext,
@Nonnull final WebAuthnAuthenticationContext context) {
- pkCred = context.getPublicKeyCredential();
- if (pkCred == null) {
+ attestation = context.getAuthenticatorAttestationResponse();
+ if (StringSupport.trimOrNull(attestation) == null) {
log.error("{} public key credential was null", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
return false;
@@ -84,7 +78,7 @@ public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationA
// TODO this should throw the error?
final boolean publicKeyCredentialIsValid =
- getWebAuthnClient().validateRegistration(context.getPublicKeyCredentialCreationOptions(), pkCred);
+ getWebAuthnClient().validateRegistration(pkCredCreationOptions, attestation);
if (!publicKeyCredentialIsValid) {
log.error("{} public key credential creation options was invalid", getLogPrefix());
@@ -95,12 +89,12 @@ public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationA
// 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());
+// final ByteArray credId = pkCred.getResponse().getAttestation().getAuthenticatorData()
+// .getAttestedCredentialData().get().getCredentialId();
+// final ByteArray credPublicKey = pkCred.getResponse().getAttestation().getAuthenticatorData()
+// .getAttestedCredentialData().get().getCredentialPublicKey();
+// context.setCredentialId(credId.getBytes());
+// context.setPublicKey(credPublicKey.getBytes());
}
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
index f4985d8..e76de02 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,16 +10,17 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
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.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;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -36,7 +37,13 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
private final Function<ProfileRequestContext, WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
/** The webauthn authentication context. */
- @NonnullBeforeExec private WebAuthnAuthenticationContext context;
+ @NonnullBeforeExec private WebAuthnAuthenticationContext context;
+
+ /** The WebAuthn client to use.*/
+ @NonnullAfterInit private WebAuthnAuthenticationClient webAuthnClient;
+
+ /** The options used to create the authentication request.*/
+ @NonnullBeforeExec private String publicKeyCredentialRequestOptions;
/** Constructor. */
public ValidateWebAuthnAssertion() {
@@ -45,6 +52,27 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
}
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ if (webAuthnClient == null) {
+ throw new ComponentInitializationException("WebAuthn client can not be null. Configuration error.");
+ }
+ super.doInitialize();
+ }
+
+
+ /**
+ * Set the WebAuthn client used to handle registration and authentication ceremonies.
+ *
+ * @param webAuthnClient The webauthnClient to set.
+ */
+ public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
+ checkSetterPreconditions();
+ webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
+ }
+
+
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
@@ -55,6 +83,13 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return false;
+ }
+ publicKeyCredentialRequestOptions = context.getPublicKeyCredentialRequestOptions();
+ if (publicKeyCredentialRequestOptions == null) {
+ log.warn("{} No public key credential request options in context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+
}
return true;
}
@@ -63,19 +98,40 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- final PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs> assertion =
- context.getAssertionResponse();
+ final String assertion = context.getAuthenticatorAssertionResponse();
+
+ context.setUsername("not-the-username");
if (assertion == null) {
- log.warn("{} No signature was found in the assertion, {} can not authenticate ",
+ log.warn("{} No authenticator assertion found, {} can not authenticate ",
getLogPrefix(),context.getUsername());
handleError(profileRequestContext, authenticationContext, "InvalidResponseType",
AuthnEventIds.INVALID_CREDENTIALS);
recordFailure(profileRequestContext);
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) {
+ log.warn("{} Error validating authenticator assertion for '{}'",
+ getLogPrefix(),context.getUsername(), e);
+ handleError(profileRequestContext, authenticationContext, "InvalidResponseType",
+ AuthnEventIds.INVALID_CREDENTIALS);
+ recordFailure(profileRequestContext);
+ return;
+ }
+
buildAuthenticationResult(profileRequestContext, authenticationContext);
}
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 09c0baf..9c70e3f 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
@@ -28,16 +28,33 @@ import com.yubico.webauthn.data.UserIdentity;
/**
* Influenced by the CredentialRegistration class in the Yubico demo libraries.
*/
+//TODO need our own storage record, so this should be test only and then replaced with the actual one eventually
public class CredentialRegistration {
UserIdentity userIdentity;
Optional<String> credentialNickname;
SortedSet<AuthenticatorTransport> transports;
-
Instant registrationTime;
RegisteredCredential credential;
-
Optional<Object> attestationMetadata;
+
+
+ public CredentialRegistration(final UserIdentity userIdentity, final Optional<String> credentialNickname,
+ final SortedSet<AuthenticatorTransport> transports, final Instant registrationTime,
+ final RegisteredCredential credential,
+ final Optional<Object> attestationMetadata) {
+ super();
+ this.userIdentity = userIdentity;
+ this.credentialNickname = credentialNickname;
+ this.transports = transports;
+ this.registrationTime = registrationTime;
+ this.credential = credential;
+ this.attestationMetadata = attestationMetadata;
+ }
+
+ public CredentialRegistration() {
+
+ }
public String getRegistrationTimestamp() {
return registrationTime.toString();
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 328de35..77f0484 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
@@ -30,8 +30,8 @@
p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
- <bean id="ValidatePublicKeyCredential" parent="AbstractWebAuthnAuthenticationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidatePublicKeyCredential" />
+ <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnAuthenticationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateAuthenticatorAttestationResponse" />
<bean id="StorePublicKeyCredential" parent="AbstractWebAuthnAuthenticationAction"
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 283c197..24012c2 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
@@ -13,13 +13,18 @@
parent="AbstractPopulateWebauthnAuthenticationContext"
p:usernameRequiredPredicate="false">
</bean>
-
- <bean id="ValidateWebAuthnAssertion" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion" />
-
- <bean id="ExtractPublicKeyAssertionFromFormRequest" scope="prototype"
+
+ <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"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+ <bean id="ValidateWebAuthnAssertion" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
+
+
</beans>
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 c05bbda..69ddf0d 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
@@ -17,41 +17,70 @@ package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
+import java.time.Instant;
+import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import java.util.TreeSet;
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.data.AuthenticatorTransport;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
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.impl.AbstractWebAuthnTest;
import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.Attestation;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.AuthenticatonExtensionsClientOutputs;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistration;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.primitive.LoggerFactory;
/**
- *
+ * Tests for {@link YubicoWebauthnAuthenticationClient}. To some extend this is testing the Yubico libraries work
+ * correctly. But it does ensure the client has been constructed to use those libraries correctly.
*/
public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest{
- private YubicoWebauthnAuthenticationClient client;
+ private final static String ORIGIN = "https://idp.example.com";
+
+ private final static String RPID = "idp.example.com";
+
+ private final static String CHALLENGE_B64 = "dGhpc2lzBaNoYWxsZW5nZQ==";
+
+ private final static String CHALLENGE_2_B64 = "8gneM8yvE20CqnSCUkyD";
+
+ private final static String USER_HANDLE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
- private final static String CHALLENGE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
+ private final static String USERNAME = "test-user";
- private final static String USER_HANDLE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(YubicoWebauthnAuthenticationClientTest.class);
+
+ private YubicoWebauthnAuthenticationClient client;
private String credentialCreationOptions;
- private final static String ORIGIN = "https://idp.example.com";
+ private String credentialRequestOptions;
- private final static String RPID = "idp.example.com";
+
+
+ private InMemoryRegistrationStorage storage;
+
+ private UserIdentity userIdentity;
@Override
@@ -59,25 +88,27 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
public void setup() throws Exception {
super.setup();
+ storage = new InMemoryRegistrationStorage();
+
final RelyingParty rp = RelyingParty.builder().identity(
RelyingPartyIdentity
.builder()
.id(RPID)
.name("Demo IdP as a WebAuthn RP")
- .build()).credentialRepository(new InMemoryRegistrationStorage())
+ .build()).credentialRepository(storage)
.allowOriginPort(true)
.allowOriginSubdomain(true)
.build();
client = new YubicoWebauthnAuthenticationClient(rp,jsonMapper);
- final UserIdentity identity =
- UserIdentity.builder().name("test-user").displayName("test user")
+ userIdentity =
+ UserIdentity.builder().name(USERNAME).displayName("test user")
.id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
final PublicKeyCredentialCreationOptions options =
PublicKeyCredentialCreationOptions.builder()
.rp(rp.getIdentity())
- .user(identity)
+ .user(userIdentity)
.challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
.pubKeyCredParams(preferredPublickeyParams)
.excludeCredentials(Optional.empty())
@@ -85,59 +116,244 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
credentialCreationOptions =
jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
+
+ final PublicKeyCredentialRequestOptions pkcro =
+ PublicKeyCredentialRequestOptions.builder()
+ .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+ .rpId(rp.getIdentity().getId())
+ .userVerification(UserVerificationRequirement.REQUIRED)
+ .timeout(Optional.of(60000l))
+ .build();
+
+ credentialRequestOptions = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(pkcro);
+
}
@Test
- public void testSuccesss() throws Exception {
+ public void testValidateRegistration_Successs() throws Exception {
- mockAuthenticator = new MockAuthenticator(ORIGIN, RPID);
+ mockAuthenticator = new MockAuthenticator(RPID);
- final Map<String, String> clientDataJson =
- mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64);
- final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
-
- final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
- final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions,
- createPublicKeyCredential(attestationObject, clientDataJsonString));
+ final var attestationJson = jsonMapper.writeValueAsString(attestation);
+ final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
assertTrue(registrationIsValid);
}
@Test
- public void testFail_BadRpId() throws Exception {
+ public void testValidateRegistration_Fail_BadRpId() throws Exception {
+
+ mockAuthenticator = new MockAuthenticator("wrong-rpid.example.com");
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
- mockAuthenticator = new MockAuthenticator(ORIGIN, "wrong-rpid.example.com");
+ final var attestationJson = jsonMapper.writeValueAsString(attestation);
+ final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
- final Map<String, String> clientDataJson =
- mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64);
- final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+ assertFalse(registrationIsValid);
+ }
- final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+ @Test
+ public void testValidateRegistrationFail_BadOrigin() throws Exception {
- final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions,
- createPublicKeyCredential(attestationObject, clientDataJsonString));
+ mockAuthenticator = new MockAuthenticator(RPID);
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", "wrong-origin", CHALLENGE_B64);
+
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final var attestationJson = jsonMapper.writeValueAsString(attestation);
+ final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+
assertFalse(registrationIsValid);
}
@Test
- public void testFail_BadOrigin() throws Exception {
+ public void testValidateAuthentication_Success() throws Exception {
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(new ByteArray(attestation.getRawId()))
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(new ByteArray(attestation.getResponse().getKey().AsCBOR().EncodeToBytes()))
+ .build();
+
+ final CredentialRegistration reg = new CredentialRegistration(userIdentity, Optional.of("Nickanme"),
+ new TreeSet<AuthenticatorTransport>(), Instant.now(), credential, Optional.empty());
+
+ storage.addRegistrationByUsername(USERNAME, reg);
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ // Now generate an assertion (authentication) and check it is valid
+ final MockAuthenticator.PublicKeyCredential<MockAuthenticator.Assertion, AuthenticatonExtensionsClientOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getRawId(), clientDataGet);
+
+ final var assertionJson = jsonMapper.writeValueAsString(assertion);
+ log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
+ final boolean valid =
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ credentialRequestOptions, assertionJson);
+ assertTrue(valid);
+
+ }
+
+ @Test
+ public void testValidateAuthentication_Fail_WrongOrigin() throws Exception {
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(new ByteArray(attestation.getRawId()))
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(new ByteArray(attestation.getResponse().getKey().AsCBOR().EncodeToBytes()))
+ .build();
+
+ final CredentialRegistration reg = new CredentialRegistration(userIdentity, Optional.of("Nickanme"),
+ new TreeSet<AuthenticatorTransport>(), Instant.now(), credential, Optional.empty());
+
+ storage.addRegistrationByUsername(USERNAME, reg);
- mockAuthenticator = new MockAuthenticator("wrong-origin", RPID);
+ // Add the wrong origin here.
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", "wrong", CHALLENGE_B64);
- final Map<String, String> clientDataJson =
- mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64);
- final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+ // Now generate an assertion (authentication) and check it is valid
+ final MockAuthenticator.PublicKeyCredential<MockAuthenticator.Assertion, AuthenticatonExtensionsClientOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getRawId(), clientDataGet);
+
+ final var assertionJson = jsonMapper.writeValueAsString(assertion);
+ log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
+ final boolean valid =
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ credentialRequestOptions, assertionJson);
+ assertFalse(valid);
+
+ }
- final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+ @Test
+ public void testValidateAuthentication_Fail_WrongOperationType() throws Exception {
- final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions,
- createPublicKeyCredential(attestationObject, clientDataJsonString));
+ mockAuthenticator = new MockAuthenticator(RPID);
- assertFalse(registrationIsValid);
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(new ByteArray(attestation.getRawId()))
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(new ByteArray(attestation.getResponse().getKey().AsCBOR().EncodeToBytes()))
+ .build();
+
+ final CredentialRegistration reg = new CredentialRegistration(userIdentity, Optional.of("Nickanme"),
+ new TreeSet<AuthenticatorTransport>(), Instant.now(), credential, Optional.empty());
+
+ storage.addRegistrationByUsername(USERNAME, reg);
+
+ // Add the wrong operation type here e.g. webauthn.create rather than webauthn.get
+ final Map<String, String> clientDataGet = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Now generate an assertion (authentication) and check it is valid
+ final MockAuthenticator.PublicKeyCredential<MockAuthenticator.Assertion, AuthenticatonExtensionsClientOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getRawId(), clientDataGet);
+
+ final var assertionJson = jsonMapper.writeValueAsString(assertion);
+ log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
+ final boolean valid =
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ credentialRequestOptions, assertionJson);
+ assertFalse(valid);
+
+ }
+
+ @Test
+ public void testValidateAuthentication_Fail_BadSignature_DifferentKey() throws Exception {
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+ mockAuthenticator.setProduceBadAssertionSignatures(true);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final MockAuthenticator.PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(new ByteArray(attestation.getRawId()))
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(new ByteArray(attestation.getResponse().getKey().AsCBOR().EncodeToBytes()))
+ .build();
+
+ final CredentialRegistration reg = new CredentialRegistration(userIdentity, Optional.of("Nickanme"),
+ new TreeSet<AuthenticatorTransport>(), Instant.now(), credential, Optional.empty());
+
+ storage.addRegistrationByUsername(USERNAME, reg);
+
+ // Add the wrong operation type here e.g. webauthn.create rather than webauthn.get
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ // Now generate an assertion (authentication) and check it is valid
+ final MockAuthenticator.PublicKeyCredential<MockAuthenticator.Assertion, AuthenticatonExtensionsClientOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getRawId(), clientDataGet);
+
+ final var assertionJson = jsonMapper.writeValueAsString(assertion);
+ log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
+ final boolean valid =
+ client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
+ credentialRequestOptions, assertionJson);
+ assertFalse(valid);
+
+ }
+
+ /**
+ * Create a client data JSON object as a Java Map.
+ *
+ * @param type the operation type
+ * @param origin the origin
+ * @param challenge the challenge
+ * @return the clientData map
+ */
+ private Map<String, String> createClientData(@Nonnull @NotEmpty final String type,
+ @Nonnull @NotEmpty final String origin,
+ @Nonnull @NotEmpty final String challenge){
+ final HashMap<String, String> obj = new HashMap<>();
+ obj.put("challenge",challenge);
+ obj.put("origin", origin);
+ obj.put("type", type);
+ return obj;
}
// Needs to do crypto checks
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 280feeb..8abc3cb 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
@@ -14,14 +14,10 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
-import static org.testng.Assert.assertNotNull;
-
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import javax.annotation.Nonnull;
-
import org.opensaml.profile.context.ProfileRequestContext;
import org.springframework.webflow.execution.RequestContext;
@@ -32,15 +28,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
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.impl.MockAuthenticator.Attestation;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
@@ -106,26 +97,7 @@ public abstract class AbstractWebAuthnTest {
}
- @Nonnull protected PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs>
- createPublicKeyCredential(final Attestation attestationObject, final String clientDataJsonString)
- throws Exception{
-
- final var response = AuthenticatorAttestationResponse.builder()
- .attestationObject(new ByteArray(attestationObject.getAttestationObjectCose()))
- .clientDataJSON(new ByteArray(clientDataJsonString.getBytes()))
- .build();
-
- final ClientRegistrationExtensionOutputs extOutputs =ClientRegistrationExtensionOutputs.builder().build();
- final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential =
- PublicKeyCredential.<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs>builder()
- .id(new ByteArray(attestationObject.getCredentialIdBytes()))
- .response(response)
- .clientExtensionResults(extOutputs)
- .build();
- assertNotNull(credential);
- assert credential != null;
- return credential;
- }
+
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
index 86e38ab..51ebed8 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
@@ -20,37 +20,64 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
import java.security.SecureRandom;
+import java.security.Signature;
import java.util.BitSet;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
-import com.yubico.webauthn.data.AuthenticatorData;
-import com.yubico.webauthn.data.ByteArray;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import COSE.AlgorithmID;
import COSE.OneKey;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
* Mock Authenticator, to create public key credentials and attestations for a given (fixed) relying party.
*
+ * <p>A new credential is created and stored for every call to
+ * {@link #createAuthenticatorAttestationResponse(String, Map, byte[])}. Calls to
+ * {@link #createAuthenticatorAssertionResponse(String, byte[], Map)} require the credentialId so it can use information
+ * stored when the credential was created. Hence to use {@link #createAuthenticatorAssertionResponse(String, byte[], Map)}
+ * you must first call {@link #createAuthenticatorAttestationResponse(String, Map, byte[])}.</p>
+ *
* <p> Does not support the attestation statement 'attStmt' (which provides data provenance information for the public
* key of the attesting authority and authenticator).</p>
*/
+ at NotThreadSafe
public class MockAuthenticator {
+ /** Fix to using this algorithm for key creation and signing. Must match KEY_ALGO. */
+ private static final String JCA_ALGO ="SHA256withECDSA";
+
+ /** Fix to using this algorithm for key creation and signing. Must match JCA_ALGO.*/
+ private static final AlgorithmID KEY_ALGO = AlgorithmID.ECDSA_256;
+
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(MockAuthenticator.class);
@@ -63,67 +90,158 @@ public class MockAuthenticator {
*/
@Nonnull @NotEmpty private final String rpId;
- /** The full RP Origin.*/
- @Nonnull @NotEmpty private final String origin;
-
/** CBOR Mapper.*/
@Nonnull private final ObjectMapper cborMapper;
+ /** Json Mapper.*/
+ @Nonnull private final ObjectMapper jsonMapper;
+
+ /** Map of base64Url encoded credentialsIds to PublicKeyCredential's.*/
+ @Nonnull
+ private final Map<String, PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs>> createdCredentialsMaps;
+
+ /** Testing flag to produce bad assertion signatures.*/
+ private boolean produceBadAssertionSignatures;
/**
*
* Constructor.
*
- * <p>Creates a new public/private key pair per instantiation.</p>
- *
- * @param the relying party's origin.
* @param relyingPartyId the relying party identifier, a valid domain string. The origin's effective domain
* (the host's domain name, no scheme and no port).
* @throws Exception on error creating this authenticator.
*/
- public MockAuthenticator(@Nonnull @NotEmpty final String rpOrigin,
- @Nonnull @NotEmpty final String relyingPartyId) throws Exception {
+ @SuppressWarnings("null")
+ public MockAuthenticator(@Nonnull @NotEmpty final String relyingPartyId) throws Exception {
rpId = Constraint.isNotEmpty(relyingPartyId, "relyingPartyId can not be null");
- origin = Constraint.isNotEmpty(rpOrigin, "rpOrigin can not be null");;
fmt = "none";
cborMapper = new ObjectMapper(new CBORFactory()).setBase64Variant(Base64Variants.MODIFIED_FOR_URL);
+ jsonMapper = JsonMapper.builder()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
+ .serializationInclusion(Include.NON_ABSENT)
+ .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+ .addModule(new Jdk8Module())
+ .addModule(new JavaTimeModule())
+ .build();
+ createdCredentialsMaps = new HashMap<>();
+ produceBadAssertionSignatures = false;
}
+ public void setProduceBadAssertionSignatures(final boolean badSigs) {
+ produceBadAssertionSignatures = badSigs;
+ }
+
+ /** Reset overall state for this authenticator.*/
+ public void reset() {
+ createdCredentialsMaps.clear();
+ }
+
/**
- * Create a WebAuthn public key credential attestation object. A new attestation is created each time.
+ * Create a WebAuthn public key credential Attestation response. A new Attestation is created and stored each time.
*
+ * @param challenge the challenge to create the response from.
+ * @param clientData the client data passed by the 'client' (browser) during authentication.
+ * @param userHandle the userID that was present in the PublicKeyCredentialCreationOptions
+ *
+ * @returns the public key credential Attestation
+ *
* @throws Exception on error.
*/
- public Attestation createCredentialAttestationObject() throws Exception {
+ public PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs>
+ createAuthenticatorAttestationResponse(@Nonnull @NotEmpty final String challenge,
+ final Map<String, String> clientData, final byte[] userHandle) throws Exception {
- final OneKey createdKey = OneKey.generateKey(AlgorithmID.ECDSA_256);
+ final OneKey createdKey = OneKey.generateKey(KEY_ALGO);
assert createdKey != null;
final String aaguidHex = generateRandomIdentifierHex(32);
- final String credentialIdHex = generateRandomIdentifierHex(32);
+ final byte[] credentialId = generateRandomIdentifierBytes(32);
+
+ final byte[] attestationObject = createAttestationObject(createdKey, aaguidHex, credentialId);
- final byte[] attestationObject = createAttestationObject(createdKey, aaguidHex, credentialIdHex);
- return new Attestation(createdKey, aaguidHex, credentialIdHex, attestationObject);
+ final String clientDataJsonString = jsonMapper.writeValueAsString(clientData);
+
+ final var publicKeyCredential = new PublicKeyCredential<Attestation, AuthenticatonExtensionsClientOutputs>(
+ credentialId,
+ new Attestation(clientDataJsonString.getBytes(), createdKey, attestationObject, userHandle),
+ new AuthenticatonExtensionsClientOutputs());
+ createdCredentialsMaps.put(Base64Support.encodeURLSafe(credentialId), publicKeyCredential);
+ return publicKeyCredential;
}
+ /**
+ * Create a WebAuthn public key credential assertion response. A new response is created each time.
+ *
+ * @param credentialId the credential identifier to give this credential, should be one that is registered.
+ * @param clientData the client data passed by the 'client' (browser) during registration.
+ *
+ * @return the public key credential assertion
+ *
+ * @throws Exception on error
+ */
+ public PublicKeyCredential<Assertion, AuthenticatonExtensionsClientOutputs> createAuthenticatorAssertionResponse(
+ @Nonnull final byte[] credentialId, final Map<String, String> clientData) throws Exception {
+
+ final var credentialb64 = Base64Support.encodeURLSafe(credentialId);
+ final var publicKeyAttestation = createdCredentialsMaps.get(credentialb64);
+ if (publicKeyAttestation == null) {
+ throw new IllegalArgumentException("Invalid credentiaId, public key attestation not found");
+ }
+
+ final byte[] rawCredentialIdentifier = credentialId;
+ final String clientDataCompactSerialization = jsonMapper.writeValueAsString(clientData);
+
+ final byte[] authenticatorData = createAuthDataForAssertion(rawCredentialIdentifier);
+ final byte[] signature =
+ sign(authenticatorData, clientDataCompactSerialization,
+ publicKeyAttestation.getResponse().getKey().AsPrivateKey());
+
+ return new PublicKeyCredential<Assertion, AuthenticatonExtensionsClientOutputs>(rawCredentialIdentifier,
+ new Assertion(clientDataCompactSerialization.getBytes(), authenticatorData, signature,
+ publicKeyAttestation.getResponse().getUserHandle()),
+ new AuthenticatonExtensionsClientOutputs());
+ }
/**
- * Create a client data JSON object as a Java Map.
+ * Create the assertion signature by signing the combined authenticatorData and clientDataJSON using the private
+ * key given.
*
- * @param challenge the challenge
- * @param type the type
- * @return the client JSON map.
+ * @param authenticatorData the authenticator data
+ * @param clientDataJSON the client data in JSON compact serialization format
+ * @param key the private key to use for signing
+ * @return the signature bytes
+ *
+ * @throws Exception on error
*/
- public Map<String, String> createClientDataJson(@Nonnull @NotEmpty final String type,
- @Nonnull @NotEmpty final String challenge){
- final HashMap<String, String> obj = new HashMap<>();
- obj.put("challenge",challenge);
- obj.put("origin", origin);
- obj.put("type", type);
- return obj;
+ private byte[] sign(final byte[] authenticatorData, final String clientDataJSON,
+ final PrivateKey key) throws Exception {
+
+ //hash the clientData JSON
+ final byte[] clientDataHash = sha256(clientDataJSON);
+ //combine together
+ final byte[] toSign = new byte[authenticatorData.length + clientDataHash.length];
+
+ System.arraycopy(authenticatorData, 0, toSign, 0, authenticatorData.length);
+ System.arraycopy(clientDataHash, 0, toSign, authenticatorData.length, clientDataHash.length);
+
+
+ final Signature sig = Signature.getInstance(JCA_ALGO);
+ if (produceBadAssertionSignatures) {
+ // Use a different key for signing
+ final OneKey newKey = OneKey.generateKey(KEY_ALGO);
+ sig.initSign(newKey.AsPrivateKey());
+ } else {
+ sig.initSign(key);
+ }
+ sig.update(toSign);
+ final byte[] signatureBytes = sig.sign();
+ return signatureBytes;
}
+
+
+
/**
@@ -140,9 +258,9 @@ public class MockAuthenticator {
* @throws Exception on error
*/
private byte[] createAttestationObject(final OneKey createdKey, final String aaguidHex,
- final String credentialId) throws Exception{
+ final byte[] credentialId) throws Exception{
final HashMap<String, Object> attObj = new HashMap<>();
- attObj.put("authData", createAuthData(createdKey, aaguidHex, credentialId).getBytes().getBytes());
+ attObj.put("authData", createAuthData(createdKey, aaguidHex, credentialId));
attObj.put("fmt", fmt);
final HashMap<String, String> attStmt = new HashMap<>();
// attStmt.put("sig", createSignature());
@@ -151,40 +269,67 @@ public class MockAuthenticator {
return cborMapper.writeValueAsBytes(attObj);
}
+ /**
+ * Create the authenticator data part of the assertion object.
+ *
+ * @param credentialId the credential identifier
+ *
+ * @return the authData
+ * @throws Exception on error
+ */
+ private byte[] createAuthDataForAssertion( @Nonnull final byte[] credentialId) throws Exception {
+ return createAuthData(null, null, credentialId);
+ }
/**
- * Create the authenticator data part of the attestation object.
- * See https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data.
+ * Create the authenticator data part of the attestation or assertion object.
*
- * @param createdKey the key to use as the basis of this attestation object (and the new key to register).
- * @param credentialId
- * @param aaguidHex
+ * @param createdKey the key to use as the basis of this attestation object (and the new key to register). Can be
+ * null if this is the result of a 'get' call during authentication.
+ * @param aaguidHex the authenticator identifier.Can be null if this is the result of a 'get' call during
+ * authentication.
+ * @param credentialId the credential identifier
+
*
* @return the authData
* @throws NoSuchAlgorithmException on error.
*/
- private AuthenticatorData createAuthData(final OneKey createdKey, final String aaguidHex,
- final String credentialId) throws Exception {
+ private byte[] createAuthData(@Nullable final OneKey createdKey, @Nullable final String aaguidHex,
+ @Nonnull final byte[] credentialId) throws Exception {
final byte[] rpIdHash = createRpIdHash();
- final byte[] flags = createFlags(true, true, true, false);
- final byte[] signCount = createSignCount(1);
- final byte[] attestedCredentialData = createAttestedCredentialData(createdKey, aaguidHex, credentialId);
- final byte[] authDataCombined = new byte[rpIdHash.length + flags.length + signCount.length
- + attestedCredentialData.length];
-
- System.arraycopy(rpIdHash, 0, authDataCombined, 0, rpIdHash.length);
- System.arraycopy(flags, 0, authDataCombined, rpIdHash.length, flags.length);
- System.arraycopy(signCount, 0, authDataCombined, rpIdHash.length + flags.length, signCount.length);
- System.arraycopy(attestedCredentialData, 0, authDataCombined, rpIdHash.length + flags.length + signCount.length,
- attestedCredentialData.length);
+ // If we have a key, we need to create the attested data and set the appropriate AT flag, otherwise do not
+ byte[] flags;
+ if (createdKey !=null) {
+ flags = createFlags(true, true, true, false);
+ } else {
+ flags = createFlags(true, true, false, false);
+ }
+ final byte[] signCount = createSignCount(1);
+ byte[] authDataCombined;
+ if (createdKey != null) {
+ final byte[] attestedCredentialData = createAttestedCredentialData(createdKey, aaguidHex, credentialId);
+
+ authDataCombined = new byte[rpIdHash.length + flags.length + signCount.length
+ + attestedCredentialData.length];
+
+ System.arraycopy(rpIdHash, 0, authDataCombined, 0, rpIdHash.length);
+ System.arraycopy(flags, 0, authDataCombined, rpIdHash.length, flags.length);
+ System.arraycopy(signCount, 0, authDataCombined, rpIdHash.length + flags.length, signCount.length);
+ System.arraycopy(attestedCredentialData, 0, authDataCombined, rpIdHash.length + flags.length +
+ signCount.length, attestedCredentialData.length);
+ } else {
+ authDataCombined = new byte[rpIdHash.length + flags.length + signCount.length];
+
+ System.arraycopy(rpIdHash, 0, authDataCombined, 0, rpIdHash.length);
+ System.arraycopy(flags, 0, authDataCombined, rpIdHash.length, flags.length);
+ System.arraycopy(signCount, 0, authDataCombined, rpIdHash.length + flags.length, signCount.length);
+ }
- final ByteArray authData = new ByteArray(authDataCombined);
- final AuthenticatorData data = new AuthenticatorData(authData);
- log.debug("Created Authenticator Data for RP, '{}' RPHash '{}', SignCounter '{}': '{}'", rpId,
- data.getRpIdHash().getHex(), data.getSignatureCounter(), data);
- return data;
+ log.debug("Created Authenticator Data for RP, '{}' RPHash '{}', SignCounter '{}', flags '{}'", rpId,
+ Hex.encodeHexString(rpIdHash), Hex.encodeHexString(signCount), Hex.encodeHexString(flags));
+ return authDataCombined;
}
/**
@@ -192,7 +337,7 @@ public class MockAuthenticator {
*
* @param up User present flag
* @param uv User verified flag
- * @param atIncluded attested credential data included flag (should be true for create credentials)
+ * @param atIncluded attested credential data included flag (should be true for create credentials, false for get)
* @param extDataIncluded extension data included flag
*
* @return a byte of the flags. Should only be one byte.
@@ -209,12 +354,20 @@ public class MockAuthenticator {
return bits.toByteArray();
}
+ /**
+ * Create the attested credential data (https://www.w3.org/TR/webauthn-2/#attestedcredentialdata)
+ *
+ * @param createdKey the public key to include in the attested credential data
+ * @param aaguidHex the aaguid as hex
+ * @param credentialId the raw credential ID
+ * @return the attestedCredentialData
+ * @throws Exception on error
+ */
private byte[] createAttestedCredentialData(final OneKey createdKey,
- final String aaguidHex, final String credentialId) throws Exception {
+ final String aaguidHex, final byte[] credentialId) throws Exception {
final byte[] aaguid = createAaguid(aaguidHex);
final byte[] credentialIdAndLength = createCredentialId(credentialId);
// OneKey is from the COSE-JAVA lib that Yubico use, but is not perhaps well maintained
- log.debug("Key is type {}", createdKey);
final byte[] coseKeyAsCborBytes = createdKey.AsCBOR().EncodeToBytes();
final byte[] attestedCredentialDataCombined = new byte[aaguid.length + credentialIdAndLength.length
@@ -230,6 +383,13 @@ public class MockAuthenticator {
return attestedCredentialDataCombined;
}
+ /**
+ * Create a 16 byte aaguid
+ *
+ * @param aaguidHex the 32 byte aaguid as Hex to convert to 16 bytes
+ * @return the 16 byte aaguid
+ * @throws DecoderException on error
+ */
private byte[] createAaguid(final String aaguidHex) throws DecoderException {
assertEquals(aaguidHex.length(), 32);
final byte[] aaguidBytes = Hex.decodeHex(aaguidHex.toCharArray());
@@ -238,12 +398,18 @@ public class MockAuthenticator {
}
+ /**
+ * Create a signature count inside 4 bytes.
+ *
+ * @param count the count as a number
+ * @return the count inside a 4 byte array
+ */
private byte[] createSignCount(final int count){
return ByteBuffer.allocate(4).putInt(count).array();
}
- private byte[] createCredentialId(final String credentialId) throws DecoderException {
- final byte[] credentialIdBytes = Hex.decodeHex(credentialId.toCharArray());
+ private byte[] createCredentialId(final byte[] credentialId) throws DecoderException {
+ final byte[] credentialIdBytes = credentialId;
final int credentialIdLength = credentialIdBytes.length;
final byte[] credentialIdLengthBytes = new byte[2];
@@ -274,6 +440,20 @@ public class MockAuthenticator {
return encodedhash;
}
+ /**
+ * Sha256 the input.
+ *
+ * @param input the input to hash
+ * @return the sha256 hashed input
+ *
+ * @throws NoSuchAlgorithmException
+ */
+ private byte[] sha256(final String input) throws NoSuchAlgorithmException {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] encodedhash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+ return encodedhash;
+ }
+
/**
* Generates a random identifier in Hex format.
*
@@ -281,6 +461,7 @@ public class MockAuthenticator {
*
* @return the randomly generated value.
*/
+ @SuppressWarnings("null")
@Nonnull static String generateRandomIdentifierHex(@Nonnull final Integer length) {
final SecureRandom secureRandom = new SecureRandom();
final StringBuilder sb = new StringBuilder();
@@ -290,67 +471,176 @@ public class MockAuthenticator {
return sb.toString().substring(0, length);
}
+ /**
+ * Generates a random identifier in bytes.
+ *
+ * @param length the length of the parameter.
+ *
+ * @return the randomly generated value.
+ * @throws Exception on error
+ */
+ @Nonnull public static byte[] generateRandomIdentifierBytes(@Nonnull final Integer length) throws Exception {
+ final SecureRandom secureRandom = new SecureRandom();
+ final byte[] randomBytes = new byte[length];
+ secureRandom.nextBytes(randomBytes);
+ return randomBytes;
+ }
+
+ //TODO not complete, can not add extensions
+ /** Client extension outputs, see https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-extension-output. */
+ public final class AuthenticatonExtensionsClientOutputs {
+
+ @JsonProperty("extensionIds")
+ public Set<String> getExtensionIds() {
+ final HashSet<String> ids = new HashSet<>();
+ return ids;
+ }
+
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ /** Public key credential container. See https://w3c.github.io/webauthn/#iface-pkcredential */
+ public final class PublicKeyCredential<T, R> {
+
+ private final byte[] id;
+
+ private final T response;
+
+ private final R clientExtensions;
+
+ public PublicKeyCredential(final byte[] id, final T response, final R clientExt) {
+ super();
+ this.id = id;
+ this.response = response;
+ clientExtensions = clientExt;
+ }
+
+ /**
+ * @return Returns the id of the credential created.
+ */
+ @JsonProperty("rawId")
+ public byte[] getRawId() {
+ return id;
+ }
+
+ @SuppressWarnings("null")
+ @JsonProperty("id")
+ public String getId() throws EncodingException {
+ return Base64Support.encodeURLSafe(id);
+ }
+
+ /**
+ * @return Returns the response.
+ */
+ @JsonProperty("response")
+ public T getResponse() {
+ return response;
+ }
+
+ @JsonProperty("type")
+ public String getType() {
+ return "public-key";
+ }
+
+ @JsonProperty("clientExtensionResults")
+ public R getClientExtensions() {
+ return clientExtensions;
+ }
+
+ }
+
+ /** Simple assertion return type so the caller can access the underlying values easily.*/
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public final class Assertion {
+
+ private final byte[] authenticatorData;
+ private final byte[] signature;
+ private final byte[] userHandle;
+ private final byte[] clientDataJSON;
+
+
+ public Assertion(@JsonProperty("clientDataJSON") final byte[] clientDataJSON,
+ @JsonProperty("authenticatorData") final byte[] authenticatorData,
+ @JsonProperty("signature") final byte[] signature,
+ @JsonProperty("userHandle") final byte[] userHandle) {
+ super();
+ this.clientDataJSON = clientDataJSON;
+ this.authenticatorData = authenticatorData;
+ this.signature = signature;
+ this.userHandle = userHandle;
+ }
+
+
+ @JsonProperty("authenticatorData")
+ public final byte[] getAuthenticatorData() {
+ return authenticatorData;
+ }
+
+ @JsonProperty("signature")
+ public final byte[] getSignature() {
+ return signature;
+ }
+
+ @JsonProperty("userHandle")
+ public final byte[] getUserHandle() {
+ return userHandle;
+ }
+
+ @JsonProperty("clientDataJSON")
+ public byte[] getClientDataJSON() {
+ return clientDataJSON;
+ }
+ }
+
/** Simple attestion return type so the caller can access the underlying values easily.*/
+ @JsonIgnoreProperties(ignoreUnknown = true)
public class Attestation {
+ /** Stash the userHandle for easy extraction.*/
+ private final byte[] userHandle;
+ /** Stash the key for easy extraction.*/
private final OneKey key;
- private final String credentialIdHex;
- private final String aaguidHex;
private final byte[] attestationObjectCose;
+ private final byte[] clientDataJSON;
/**
* Constructor.
*
+ * @param clientDataJSON the client data in compact JSON serialization format.
* @param key the key
- * @param credentialIdHex the credential Id
* @param aaguidHex the aaguid
* @param attestationObjectCose the attestation object
*/
- public Attestation(final OneKey key, final String credentialIdHex, final String aaguidHex,
- final byte[] attestationObjectCose) {
+ public Attestation(@JsonProperty("clientDataJSON") final byte[] clientDataJSON,
+ final OneKey key,
+ @JsonProperty("attestationObject") final byte[] attestationObjectCose,
+ final byte[] userHandle) {
super();
+ this.clientDataJSON = clientDataJSON;
this.key = key;
- this.credentialIdHex = credentialIdHex;
- this.aaguidHex = aaguidHex;
this.attestationObjectCose = attestationObjectCose;
+ this.userHandle = userHandle;
}
- /**
- * @return Returns the key.
- */
- public OneKey getKey() {
- return key;
+ @JsonIgnore
+ public byte[] getUserHandle() {
+ return userHandle;
}
- /**
- * @return Returns the credentialIdHex.
- */
- public String getCredentialIdHex() {
- return credentialIdHex;
- }
-
- public byte[] getCredentialIdBytes() throws DecoderException {
- return Hex.decodeHex(credentialIdHex.toCharArray());
- }
-
- public byte[] getAaguidBytes() throws DecoderException {
- return Hex.decodeHex(aaguidHex.toCharArray());
- }
-
- /**
- * @return Returns the aaguidHex.
- */
- public String getAaguidHex() {
- return aaguidHex;
- }
+ @JsonIgnore
+ public OneKey getKey() {
+ return key;
+ }
- /**
- * @return Returns the attestationObjectCose.
- */
+ @JsonProperty("attestationObject")
public byte[] getAttestationObjectCose() {
return attestationObjectCose;
}
-
+
+ @JsonProperty("clientDataJSON")
+ public byte[] getClientDataJSON() {
+ return clientDataJSON;
+ }
}
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 a435aab..9634247 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
@@ -14,16 +14,11 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
-import static org.testng.Assert.assertNull;
-
-import java.util.Map;
import java.util.Optional;
-import org.springframework.webflow.execution.Event;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.yubico.webauthn.RelyingParty;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
@@ -32,17 +27,16 @@ import com.yubico.webauthn.data.UserIdentity;
import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnAuthenticationClient;
-import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.Attestation;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
import net.shibboleth.shared.codec.Base64Support;
/**
- * Tests for {@link ValidatePublicKeyCredential}.
+ * Tests for {@link ValidateAuthenticatorAttestationResponse}.
*/
public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
- private ValidatePublicKeyCredential validator;
+ private ValidateAuthenticatorAttestationResponse validator;
private final static String CHALLENGE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
@@ -52,7 +46,7 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
@BeforeMethod
public void setup() throws Exception {
super.setup();
- validator = new ValidatePublicKeyCredential();
+ validator = new ValidateAuthenticatorAttestationResponse();
//Move this test to one of the client, this should use a mock and less specific data types
final RelyingParty rp = RelyingParty.builder().identity(
RelyingPartyIdentity
@@ -88,17 +82,17 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
@Test
public void testSuccesss() throws Exception {
- mockAuthenticator = new MockAuthenticator("https://idp.example.com", "idp.example.com");
-
- final Map<String, String> clientDataJson =
- mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64);
- final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
-
- final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
- webAuthnContext.setPublicKeyCredential(createPublicKeyCredential(attestationObject, clientDataJsonString));
-
- final Event event = validator.execute(src);
- assertNull(event);
+// mockAuthenticator = new MockAuthenticator("https://idp.example.com");
+//
+// final Map<String, String> clientDataJson =
+// mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64);
+// final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+//
+// final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+// webAuthnContext.setPublicKeyCredential(createPublicKeyCredential(attestationObject, clientDataJsonString));
+//
+// final Event event = validator.execute(src);
+// assertNull(event);
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list