[java-idp-plugin-webauthn] branch main updated: Add unit tests
Phil Smart
philip.smart at jisc.ac.uk
Wed Mar 20 17:11:44 UTC 2024
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=26c070d3415fb863f932f5a2d2b4a3a2d2c20535
The following commit(s) were added to refs/heads/main by this push:
new 26c070d Add unit tests
26c070d is described below
commit 26c070d3415fb863f932f5a2d2b4a3a2d2c20535
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Mar 20 17:11:40 2024 +0000
Add unit tests
---
.../client/WebAuthnAuthenticationClient.java | 2 +-
.../context/WebAuthnAuthenticationContext.java | 27 ---
.../authn/webauthn/admin/impl/AddUserId.java | 2 +-
.../storage/impl/InMemoryRegistrationStorage.java | 213 ---------------------
.../AddAttestationConveyancePreferenceTest.java | 108 +++++++++++
.../AddAuthenticatorAttachmentRequirementTest.java | 87 +++++++++
.../admin/impl/AddResidentKeyRequirementTest.java | 87 +++++++++
.../authn/webauthn/admin/impl/AddUserIdTest.java | 188 ++++++++++++++++++
.../impl/ExceptionThrowingMockWebAuthnClient.java | 69 +++++++
.../YubicoWebauthnAuthenticationClientTest.java | 9 +-
.../authn/webauthn/impl/AbstractWebAuthnTest.java | 77 ++++++++
.../impl/AddUserVerificationRequirementTest.java | 90 +++++++++
...reatePublicKeyCredentialRequestOptionsTest.java | 124 ++++++++++++
.../impl/ValidatePublicKeyCredentialTest.java | 48 +----
.../impl/CredentialRegistrationSerializerTest.java | 4 +-
...IdPStorageServiceCredentialRespositoryTest.java | 3 +-
.../storage/impl/InMemoryRegistrationStorage.java | 170 ++++++++++++++++
17 files changed, 1013 insertions(+), 295 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
index bbf3da3..a248160 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
@@ -37,7 +37,7 @@ public interface WebAuthnAuthenticationClient {
*
* @param requestParams the options that should be present in the authentication request.
*
- * @return a PublicKeyCredentialRequestOptions object to supply the WebAuthn 'get' call
+ * @return a PublicKeyCredentialRequestOptions object to supply to the WebAuthn 'get' call
*
* @throws WebAuthnAuthenticationClientException if there is an error generating the authentication request
*
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 e5b026e..4c98aef 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
@@ -25,10 +25,6 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
/** The public key credential request options for authentication.*/
@Nullable private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
-
- // TODO maybe generate the JSON during the webflow action to populate the view
- /** The public key credential request options for authentication represented as a JSON string.*/
- @Nullable private String publicKeyCredentialRequestOptionsJSON;
/**
* Get the credential Id.
@@ -97,28 +93,5 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
getAuthenticatorAssertionResponse() {
return authenticatorAssertionResponse;
}
-
-
- /**
- * Get the public key credential request options to use in JSON format.
- *
- * @return the publicKeyCredentialRequestOptions in JSON format.
- */
- public String getPublicKeyCredentialRequestOptionsJSON() {
- return publicKeyCredentialRequestOptionsJSON;
- }
-
- /**
- * Set the public key credential request options to use in JSON format.
- *
- * @param requestOptionsJSON The options to set.
- *
- * @return this context
- */
- public WebAuthnAuthenticationContext setPublicKeyCredentialRequestOptionsJSON(
- @Nullable final String requestOptionsJSON) {
- publicKeyCredentialRequestOptionsJSON = requestOptionsJSON;
- return this;
- }
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
index 08b7621..ec72a7b 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
@@ -88,7 +88,7 @@ public class AddUserId extends AbstractWebAuthnRegistrationAction {
username = context.getUsername();
if (username == null) {
log.error("{} Username not available in registration context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
return false;
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
deleted file mode 100644
index 5a82ec2..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright (c) 2018, Yubico AB
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-// list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-// this list of conditions and the following disclaimer in the documentation
-// and/or other materials provided with the distribution.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.yubico.webauthn.AssertionResult;
-import com.yubico.webauthn.RegisteredCredential;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
-
-import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
-
-/**
- * Use {@link IdPStorageServiceCredentialRespository}
- */
-// move to test package
- at Deprecated
-public class InMemoryRegistrationStorage implements StorageServiceCredentialRepository {
-
- private final Cache<String, Set<CredentialRegistration>> storage =
- CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(1, TimeUnit.DAYS).build();
-
- private static final Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class);
-
- ////////////////////////////////////////////////////////////////////////////////
- // The following methods are required by the CredentialRepository interface.
- ////////////////////////////////////////////////////////////////////////////////
-
- @Override
- public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
- return getRegistrationsByUsername(username).stream()
- .map(
- registration ->
- PublicKeyCredentialDescriptor.builder()
- .id(registration.getCredential().getCredentialId())
- .transports(registration.getTransports())
- .build())
- .collect(Collectors.toSet());
- }
-
- @Override
- public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
- return getRegistrationsByUserHandle(userHandle).stream()
- .findAny()
- .map(CredentialRegistration::getUsername);
- }
-
- @Override
- public Optional<ByteArray> getUserHandleForUsername(final String username) {
- return getRegistrationsByUsername(username).stream()
- .findAny()
- .map(reg -> reg.getUserIdentity().getId());
- }
-
- @Override
- public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
- final Optional<CredentialRegistration> registrationMaybe =
- storage.asMap().values().stream()
- .flatMap(Collection::stream)
- .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId()))
- .findAny();
-
- logger.debug(
- "lookup credential ID: {}, user handle: {}; result: {}",
- credentialId,
- userHandle,
- registrationMaybe);
- return registrationMaybe.map(
- registration ->
- RegisteredCredential.builder()
- .credentialId(registration.getCredential().getCredentialId())
- .userHandle(registration.getUserIdentity().getId())
- .publicKeyCose(registration.getCredential().getPublicKeyCose())
- .signatureCount(registration.getCredential().getSignatureCount())
- .build());
- }
-
- @Override
- public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
- return Collections.unmodifiableSet(
- storage.asMap().values().stream()
- .flatMap(Collection::stream)
- .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
- .map(
- reg ->
- RegisteredCredential.builder()
- .credentialId(reg.getCredential().getCredentialId())
- .userHandle(reg.getUserIdentity().getId())
- .publicKeyCose(reg.getCredential().getPublicKeyCose())
- .signatureCount(reg.getCredential().getSignatureCount())
- .build())
- .collect(Collectors.toSet()));
- }
-
- ////////////////////////////////////////////////////////////////////////////////
- // The following methods are specific to this demo application.
- ////////////////////////////////////////////////////////////////////////////////
-
- public boolean addRegistrationByUsername(final String username, final CredentialRegistration reg) {
- try {
- return storage.get(username, HashSet::new).add(reg);
- } catch (final ExecutionException e) {
- logger.error("Failed to add registration", e);
- throw new RuntimeException(e);
- }
- }
-
- public Set<CredentialRegistration> getRegistrationsByUsername(final String username) {
- try {
- return storage.get(username, HashSet::new);
- } catch (final ExecutionException e) {
- logger.error("Registration lookup failed", e);
- throw new RuntimeException(e);
- }
- }
-
- public Collection<CredentialRegistration> getRegistrationsByUserHandle(final ByteArray userHandle) {
- return storage.asMap().values().stream()
- .flatMap(Collection::stream)
- .filter(
- credentialRegistration ->
- userHandle.equals(credentialRegistration.getUserIdentity().getId()))
- .collect(Collectors.toList());
- }
-
- public void updateSignatureCount(final AssertionResult result) {
- final CredentialRegistration registration =
- getRegistrationByUsernameAndCredentialId(
- result.getUsername(), result.getCredential().getCredentialId())
- .orElseThrow(
- () ->
- new NoSuchElementException(
- String.format(
- "Credential \"%s\" is not registered to user \"%s\"",
- result.getCredential().getCredentialId(), result.getUsername())));
-
- final Set<CredentialRegistration> regs = storage.getIfPresent(result.getUsername());
- regs.remove(registration);
- regs.add(
- registration.withCredential(
- registration.getCredential().toBuilder()
- .signatureCount(result.getSignatureCount())
- .build()));
- }
-
- public Optional<CredentialRegistration> getRegistrationByUsernameAndCredentialId(
- final String username, final ByteArray id) {
- try {
- return storage.get(username, HashSet::new).stream()
- .filter(credReg -> id.equals(credReg.getCredential().getCredentialId()))
- .findFirst();
- } catch (final ExecutionException e) {
- logger.error("Registration lookup failed", e);
- throw new RuntimeException(e);
- }
- }
-
- public boolean removeRegistrationByUsername(
- final String username, final CredentialRegistration credentialRegistration) {
- try {
- return storage.get(username, HashSet::new).remove(credentialRegistration);
- } catch (final ExecutionException e) {
- logger.error("Failed to remove registration", e);
- throw new RuntimeException(e);
- }
- }
-
- public boolean removeAllRegistrations(final String username) {
- storage.invalidate(username);
- return true;
- }
-
- public boolean userExists(final String username) {
- return !getRegistrationsByUsername(username).isEmpty();
- }
-}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAttestationConveyancePreferenceTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAttestationConveyancePreferenceTest.java
new file mode 100644
index 0000000..f1e1059
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAttestationConveyancePreferenceTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.AttestationConveyancePreference;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for AddAttestationConveyancePreference
+ */
+public class AddAttestationConveyancePreferenceTest extends AbstractWebAuthnTest {
+
+ private AddAttestationConveyancePreference addAction;
+
+ private WebAuthnRegistrationContext context;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+ addAction = new AddAttestationConveyancePreference();
+ addAction.setWebAuthnClient(client);
+ addAction.setCredentialRepository(credentialRepo);
+ }
+
+
+ @Test
+ public void testAddAttestationConveyancePreference_Default() throws ComponentInitializationException {
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAttestationConveyancePreference());
+ assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.NONE);
+ }
+
+ @Test
+ public void testAddAttestationConveyancePreference_Direct() throws ComponentInitializationException {
+ addAction.setAttestationConveyancePreference("direct");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAttestationConveyancePreference());
+ assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.DIRECT);
+ }
+
+ @Test
+ public void testAddAttestationConveyancePreference_Indirect() throws ComponentInitializationException {
+ addAction.setAttestationConveyancePreference("indirect");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAttestationConveyancePreference());
+ assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.INDIRECT);
+ }
+
+ @Test
+ public void testAddAttestationConveyancePreference_Enterprise() throws ComponentInitializationException {
+ addAction.setAttestationConveyancePreference("enterprise");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAttestationConveyancePreference());
+ assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.ENTERPRISE);
+ }
+
+ @Test
+ public void testAddAttestationConveyancePreference_None() throws ComponentInitializationException {
+ addAction.setAttestationConveyancePreference("none");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAttestationConveyancePreference());
+ assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.NONE);
+ }
+
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testAddAttestationConveyancePreference_Unknown() throws ComponentInitializationException {
+ addAction.setAttestationConveyancePreference("unknown");
+ addAction.initialize();
+ addAction.execute(src);
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirementTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirementTest.java
new file mode 100644
index 0000000..bc02424
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirementTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddAuthenticatorAttachmentRequirement}
+ */
+public class AddAuthenticatorAttachmentRequirementTest extends AbstractWebAuthnTest {
+
+ private AddAuthenticatorAttachmentRequirement addAction;
+
+ private WebAuthnRegistrationContext context;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+ addAction = new AddAuthenticatorAttachmentRequirement();
+ addAction.setWebAuthnClient(client);
+ addAction.setCredentialRepository(credentialRepo);
+ }
+
+ @Test
+ public void testAddAuthenticatorAttachmentRequirement_CrossPlatform() throws ComponentInitializationException {
+ addAction.setAuthenticatorAttachmentRequirement("cross-platform");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAuthenticatorAttachmentRequirement());
+ assertEquals(context.getAuthenticatorAttachmentRequirement(), AuthenticatorAttachment.CROSS_PLATFORM);
+ }
+
+ @Test
+ public void testAddAuthenticatorAttachmentRequirement_Platform() throws ComponentInitializationException {
+ addAction.setAuthenticatorAttachmentRequirement("platform");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getAuthenticatorAttachmentRequirement());
+ assertEquals(context.getAuthenticatorAttachmentRequirement(), AuthenticatorAttachment.PLATFORM);
+ }
+
+ @Test
+ public void testAddAuthenticatorAttachmentRequirement_Default() throws ComponentInitializationException {
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNull(context.getAuthenticatorAttachmentRequirement());
+ }
+
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testAddAuthenticatorAttachmentRequirement_Uknown() throws ComponentInitializationException {
+ addAction.setAuthenticatorAttachmentRequirement("unkown");
+ addAction.initialize();
+ addAction.execute(src);
+ }
+
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirementTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirementTest.java
new file mode 100644
index 0000000..4e9fcb5
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirementTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddResidentKeyRequirement}
+ */
+public class AddResidentKeyRequirementTest extends AbstractWebAuthnTest {
+
+ private AddResidentKeyRequirement addAction;
+
+ private WebAuthnRegistrationContext context;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+ addAction = new AddResidentKeyRequirement();
+ addAction.setWebAuthnClient(client);
+ addAction.setCredentialRepository(credentialRepo);
+ }
+
+ @Test
+ public void testAddResidentKeyRequirement_Default() throws ComponentInitializationException {
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getResidentKeyRequirement());
+ assertEquals(context.getResidentKeyRequirement(), ResidentKeyRequirement.PREFERRED);
+ }
+
+ @Test
+ public void testAddResidentKeyRequirement_Discouraged() throws ComponentInitializationException {
+ addAction.setResidentKeyRequirement("discouraged");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getResidentKeyRequirement());
+ assertEquals(context.getResidentKeyRequirement(), ResidentKeyRequirement.DISCOURAGED);
+ }
+
+ @Test
+ public void testAddResidentKeyRequirement_Required() throws ComponentInitializationException {
+ addAction.setResidentKeyRequirement("required");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getResidentKeyRequirement());
+ assertEquals(context.getResidentKeyRequirement(), ResidentKeyRequirement.REQUIRED);
+ }
+
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testAddResidentKeyRequirement_Unknown() throws ComponentInitializationException {
+ addAction.setResidentKeyRequirement("unknown");
+ addAction.initialize();
+ addAction.execute(src);
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
new file mode 100644
index 0000000..8fb4ad3
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.TreeSet;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.UserIdentity;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+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.storage.CredentialRegistration;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddUserId}.
+ */
+public class AddUserIdTest extends AbstractWebAuthnTest {
+
+ private AddUserId addAction;
+
+ private WebAuthnRegistrationContext context;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+ addAction = new AddUserId();
+ addAction.setWebAuthnClient(client);
+ addAction.setCredentialRepository(credentialRepo);
+ }
+
+ @Test
+ public void testNewUserId() throws ComponentInitializationException {
+ addAction.initialize();
+
+ context.setUsername("jdoe");
+
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getUserId());
+ }
+
+ @Test
+ public void testNewUserId_CustomGenerator() throws ComponentInitializationException {
+ addAction.setUserIdGeneratorStrategy(input -> new byte[] {(byte)0xFF});
+ addAction.initialize();
+
+ context.setUsername("jdoe");
+
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getUserId());
+ final byte[] userId = context.getUserId();
+ assert userId != null;
+ assertEquals(userId.length,1);
+ assertEquals(userId[0], (byte)0xFF);
+ }
+
+ @Test
+ public void testNewUserId_ExistingUserHandle() throws Exception {
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final var user = UserIdentity.builder()
+ .name("jdoe")
+ .displayName("John Doe")
+ .id(ByteArray.fromBase64(USER_HANDLE_B64))
+ .build();
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final var barray = ByteArray.fromBase64(USER_HANDLE_B64);
+ assert barray.getBase64().equals(USER_HANDLE_B64);
+ assert Arrays.equals(barray.getBytes(), Base64Support.decode(USER_HANDLE_B64));
+
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(attestation.getId())
+ .userHandle(ByteArray.fromBase64(USER_HANDLE_B64))
+ .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialPublicKey())
+ .build();
+
+ final var reg = CredentialRegistration.builder()
+ .withUserIdentity(user)
+ .withTransports(new TreeSet<AuthenticatorTransport>())
+ .withRegistrationTime(Instant.now())
+ .withCredential(credential)
+ .withAttestationMetadata(CollectionSupport.emptySet())
+ .withCredentialNickname("nickname")
+ .withDiscoverable(Optional.of(Boolean.TRUE))
+ .withUserVerified(true)
+ .build();
+
+ credentialRepo.addRegistrationByUsername("jdoe", reg);
+ addAction.initialize();
+
+ context.setUsername("jdoe");
+
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(context.getUserId());
+ final byte[] userId = context.getUserId();
+ assert userId != null;
+ assertEquals(userId.length,Base64Support.decode(USER_HANDLE_B64).length);
+ assertEquals(userId, Base64Support.decode(USER_HANDLE_B64));
+ }
+
+ @Test
+ public void testNewUserId_NullId() throws ComponentInitializationException {
+ addAction.setUserIdGeneratorStrategy(input -> null);
+ addAction.initialize();
+
+ context.setUsername("jdoe");
+
+ final Event result = addAction.execute(src);
+ assertNotNull(result);
+ assert result != null;
+ assertEquals(result.getId(), "InvalidRegistration");
+ }
+
+ @Test
+ public void testNewUserId_NoUsername() throws ComponentInitializationException {
+ addAction.setUserIdGeneratorStrategy(input -> new byte[] {(byte)0xFF});
+ addAction.initialize();
+
+ context.setUsername(null);
+
+ final Event result = addAction.execute(src);
+ assertNotNull(result);
+ assert result != null;
+ assertEquals(result.getId(), "InvalidRegistrationContext");
+ }
+
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testNewUserId_MoreThan64Bytes() throws ComponentInitializationException {
+ addAction.setUserIdGeneratorStrategy(input -> {
+ final Random random = new Random();
+ final byte[] byteArray = new byte[65];
+ random.nextBytes(byteArray);
+ return byteArray;
+ });
+ addAction.initialize();
+ context.setUsername("jdoe");
+ addAction.execute(src);
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/ExceptionThrowingMockWebAuthnClient.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/ExceptionThrowingMockWebAuthnClient.java
new file mode 100644
index 0000000..176c35e
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/ExceptionThrowingMockWebAuthnClient.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
+
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.exception.AssertionFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.exception.RegistrationFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
+
+/**
+ * Mock client that just throws exceptions
+ */
+public class ExceptionThrowingMockWebAuthnClient implements WebAuthnAuthenticationClient{
+
+ @Override
+ public RegistrationResult validateAuthenticatorAttestationResponse(
+ final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions,
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+ authenticatorAttestationResponse)
+ throws RegistrationFailureException {
+ throw new RegistrationFailureException("Unable to validate authenticator attestation response");
+ }
+
+ @Override
+ public AssertionResult validateAuthenticatorAssertionResponse(final String username, final byte[] userId,
+ final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ authenticatorAssertionResponse)
+ throws AssertionFailureException {
+ throw new AssertionFailureException("Unable to validate authenticator assertion response");
+ }
+
+ @Override
+ public PublicKeyCredentialCreationOptions createRegistrationRequest(
+ final CredentialCreationOptionsParameters creationOptions) throws WebAuthnAuthenticationClientException {
+ throw new WebAuthnAuthenticationClientException("Unable to create registration request");
+ }
+
+ @Override
+ public PublicKeyCredentialRequestOptions createAuthenticationRequest(
+ final CredentialRequestOptionsParameters requestParams) throws WebAuthnAuthenticationClientException {
+ throw new WebAuthnAuthenticationClientException("Unable to create authentication request");
+ }
+
+}
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 e86adf0..b4c0cbc 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
@@ -49,6 +49,7 @@ import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.collection.CollectionSupport;
/**
* Tests for {@link YubicoWebAuthnAuthenticationClient}. To some extend this is testing the Yubico libraries work
@@ -187,7 +188,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
.withTransports(new TreeSet<AuthenticatorTransport>())
.withRegistrationTime(Instant.now())
.withCredential(credential)
- .withAttestationMetadata(null)
+ .withAttestationMetadata(CollectionSupport.emptySet())
.withCredentialNickname("Nickname")
.withDiscoverable(Optional.of(Boolean.TRUE))
.withUserVerified(true)
@@ -234,7 +235,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
.withTransports(new TreeSet<AuthenticatorTransport>())
.withRegistrationTime(Instant.now())
.withCredential(credential)
- .withAttestationMetadata(null)
+ .withAttestationMetadata(CollectionSupport.emptySet())
.withCredentialNickname("Nickname")
.withDiscoverable(Optional.of(Boolean.TRUE))
.withUserVerified(true)
@@ -279,7 +280,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
.withTransports(new TreeSet<AuthenticatorTransport>())
.withRegistrationTime(Instant.now())
.withCredential(credential)
- .withAttestationMetadata(null)
+ .withAttestationMetadata(CollectionSupport.emptySet())
.withCredentialNickname("Nickname")
.withDiscoverable(Optional.of(Boolean.TRUE))
.withUserVerified(true)
@@ -325,7 +326,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
.withTransports(new TreeSet<AuthenticatorTransport>())
.withRegistrationTime(Instant.now())
.withCredential(credential)
- .withAttestationMetadata(null)
+ .withAttestationMetadata(CollectionSupport.emptySet())
.withCredentialNickname("Nickname")
.withDiscoverable(Optional.of(Boolean.TRUE))
.withUserVerified(true)
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 098391a..4bbc26b 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
@@ -18,6 +18,7 @@ import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import javax.annotation.Nonnull;
@@ -31,14 +32,25 @@ 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.RelyingParty;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
import com.yubico.webauthn.data.PublicKeyCredentialParameters;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+import com.yubico.webauthn.data.UserIdentity;
import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.collection.CollectionSupport;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
@@ -79,6 +91,12 @@ public abstract class AbstractWebAuthnTest {
/** The CBOR friendly json mapper.*/
protected ObjectMapper jsonMapper;
+ /** The WebAuthn Client to test with.*/
+ protected WebAuthnAuthenticationClient client;
+
+ /** A mocked credential repository to use.*/
+ protected StorageServiceCredentialRepository credentialRepo;
+
/** List of acceptable public key algorithms.*/
@Nonnull protected final List<PublicKeyCredentialParameters> preferredPublickeyParams =
CollectionSupport.listOf(
@@ -119,8 +137,67 @@ public abstract class AbstractWebAuthnTest {
webAuthnRegContext = new WebAuthnRegistrationContext();
prc.addSubcontext(webAuthnRegContext);
+
+ //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
+ .builder()
+ .id("idp.example.com")
+ .name("Demo IdP as a WebAuthn RP")
+ .build()).credentialRepository(new InMemoryRegistrationStorage())
+ .allowOriginPort(true)
+ .allowOriginSubdomain(true)
+ .build();
+
+ final UserIdentity identity =
+ UserIdentity.builder().name("test-user").displayName("test user")
+ .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
+
+ final PublicKeyCredentialCreationOptions credentialCreationOptions =
+ PublicKeyCredentialCreationOptions.builder()
+ .rp(rp.getIdentity())
+ .user(identity)
+ .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+ .pubKeyCredParams(preferredPublickeyParams)
+ .excludeCredentials(Optional.empty())
+ .timeout(Optional.empty()).build();
+
+ webAuthnRegContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
+ // TODO Should create a mock factory
+ final YubicoWebauthnClientFactory factory = new YubicoWebauthnClientFactory();
+ factory.setPreferredPublickeyParams(preferredPublickeyParams.stream().map(alg -> alg.getAlg().name()).toList());
+ factory.setCredentialRepository(new InMemoryRegistrationStorage());
+ factory.setRelyingPartyId("idp.example.com");
+ factory.setRelyingPartyName("Demo IdP as a WebAuthn RP");
+ factory.initialize();
+ client = factory.getObject();
+
+ // The im-memory repo is for testing only
+ credentialRepo = new InMemoryRegistrationStorage();
}
+
+ /**
+ * Add a WebAuthn registration context to the profile request context
+ */
+ protected WebAuthnRegistrationContext addWebAuthnRegistrationContext() {
+ return prc.ensureSubcontext(WebAuthnRegistrationContext.class);
+ }
+
+ /**
+ * Add a Base WebAuthn registration context to the authentication context
+ */
+ protected BaseWebAuthnContext addBaseWebAuthnRegistrationContext() {
+ return prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(BaseWebAuthnContext.class);
+ }
+
+ /**
+ * Add a WebAuthn authentication context to the authentication context
+ */
+ protected WebAuthnAuthenticationContext addWebAuthnAuthenticationContext() {
+ return prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(WebAuthnAuthenticationContext.class);
+ }
+
/**
* Create a running server that mimics responses from a WebAuthn Metadata Service.
* Creates a new self-signed certificate.
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirementTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirementTest.java
new file mode 100644
index 0000000..3040ac2
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirementTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddUserVerificationRequirement}
+ */
+public class AddUserVerificationRequirementTest extends AbstractWebAuthnTest {
+
+ private AddUserVerificationRequirement addAction;
+
+ private BaseWebAuthnContext baseContext;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ baseContext = addBaseWebAuthnRegistrationContext();
+ addAction = new AddUserVerificationRequirement();
+ addAction.setWebAuthnClient(client);
+ addAction.setCredentialRepository(credentialRepo);
+ }
+
+
+ @Test
+ public void testAddUserVerificationRequirement_Default() throws ComponentInitializationException {
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(baseContext.getUserVerificationRequirement());
+ assertEquals(baseContext.getUserVerificationRequirement(), UserVerificationRequirement.PREFERRED);
+
+ }
+
+ @Test
+ public void testAddUserVerificationRequirement_Required() throws ComponentInitializationException {
+ addAction.setUserVerificationRequirement("required");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(baseContext.getUserVerificationRequirement());
+ assertEquals(baseContext.getUserVerificationRequirement(), UserVerificationRequirement.REQUIRED);
+
+ }
+
+ @Test
+ public void testAddUserVerificationRequirement_Discouraged() throws ComponentInitializationException {
+ addAction.setUserVerificationRequirement("discouraged");
+ addAction.initialize();
+ final Event result = addAction.execute(src);
+ assertNull(result);
+ assertNotNull(baseContext.getUserVerificationRequirement());
+ assertEquals(baseContext.getUserVerificationRequirement(), UserVerificationRequirement.DISCOURAGED);
+
+ }
+
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testAddUserVerificationRequirement_Unknown() throws ComponentInitializationException {
+ addAction.setUserVerificationRequirement("unknown");
+ addAction.initialize();
+ addAction.execute(src);
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptionsTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptionsTest.java
new file mode 100644
index 0000000..67e822b
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptionsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.util.Random;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.client.impl.ExceptionThrowingMockWebAuthnClient;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Test for {@link CreatePublicKeyCredentialRequestOptions}
+ */
+public class CreatePublicKeyCredentialRequestOptionsTest extends AbstractWebAuthnTest {
+
+ private CreatePublicKeyCredentialRequestOptions action;
+
+ private WebAuthnAuthenticationContext context;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnAuthenticationContext();
+
+ action = new CreatePublicKeyCredentialRequestOptions();
+ action.setWebAuthnClient(client);
+ }
+
+ @Test
+ public void testCreatePublicKeyCredentialRequestOptions() throws ComponentInitializationException {
+
+ final Random random = new Random();
+ final byte[] challenge = new byte[64];
+ random.nextBytes(challenge);
+ context.setServerChallenge(challenge);
+ context.setUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED);
+
+ action.initialize();
+
+ final Event event = action.execute(src);
+ assertNull(event);
+ assertNotNull(context.getPublicKeyCredentialRequestOptions());
+ final var options = context.getPublicKeyCredentialRequestOptions();
+ assert options != null;
+ assertEquals(options.getChallenge().compareTo(new ByteArray(context.getServerChallenge())),0);
+ assertNotNull(options.getRpId());
+ assertNotNull(options.getUserVerification());
+
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testCreatePublicKeyCredentialRequestOptions_NoChallenge() throws ComponentInitializationException {
+
+ context.setUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED);
+ action.initialize();
+
+ final Event event = action.execute(src);
+ assertNotNull(event);
+ assertEquals(event.getId(),AuthnEventIds.AUTHN_EXCEPTION);
+
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testCreatePublicKeyCredentialRequestOptions_NoUvRequirement() throws ComponentInitializationException {
+ final Random random = new Random();
+ final byte[] challenge = new byte[64];
+ random.nextBytes(challenge);
+ context.setServerChallenge(challenge);
+
+ action.initialize();
+
+ final Event event = action.execute(src);
+ assertNotNull(event);
+ assertEquals(event.getId(),AuthnEventIds.AUTHN_EXCEPTION);
+
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testCreatePublicKeyCredentialRequestOptions_WebAuthnException() throws ComponentInitializationException {
+ final Random random = new Random();
+ final byte[] challenge = new byte[64];
+ random.nextBytes(challenge);
+ context.setServerChallenge(challenge);
+ context.setUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED);
+
+ action.setWebAuthnClient(new ExceptionThrowingMockWebAuthnClient());
+
+ action.initialize();
+
+ final Event event = action.execute(src);
+ assertNotNull(event);
+ assertEquals(event.getId(),AuthnEventIds.AUTHN_EXCEPTION);
+
+ }
+
+}
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 3770d93..81f51b5 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,22 +14,10 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
-import java.util.Optional;
-
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.yubico.webauthn.RelyingParty;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
-import com.yubico.webauthn.data.RelyingPartyIdentity;
-import com.yubico.webauthn.data.UserIdentity;
-
import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse;
-import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
-import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
-import net.shibboleth.shared.codec.Base64Support;
/**
@@ -47,41 +35,9 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
@BeforeMethod
public void setup() throws Exception {
super.setup();
- 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
- .builder()
- .id("idp.example.com")
- .name("Demo IdP as a WebAuthn RP")
- .build()).credentialRepository(new InMemoryRegistrationStorage())
- .allowOriginPort(true)
- .allowOriginSubdomain(true)
- .build();
-
- final UserIdentity identity =
- UserIdentity.builder().name("test-user").displayName("test user")
- .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
-
- final PublicKeyCredentialCreationOptions credentialCreationOptions =
- PublicKeyCredentialCreationOptions.builder()
- .rp(rp.getIdentity())
- .user(identity)
- .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
- .pubKeyCredParams(preferredPublickeyParams)
- .excludeCredentials(Optional.empty())
- .timeout(Optional.empty()).build();
-
- webAuthnRegContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
- final YubicoWebauthnClientFactory factory = new YubicoWebauthnClientFactory();
- factory.setPreferredPublickeyParams(preferredPublickeyParams.stream().map(alg -> alg.getAlg().name()).toList());
- factory.setCredentialRepository(new InMemoryRegistrationStorage());
- factory.setRelyingPartyId("idp.example.com");
- factory.setRelyingPartyName("Demo IdP as a WebAuthn RP");
- factory.initialize();
- final WebAuthnAuthenticationClient client = factory.getObject();
+ validator = new ValidateAuthenticatorAttestationResponse();
validator.setWebAuthnClient(client);
- validator.setCredentialRepository(new InMemoryRegistrationStorage());
+ validator.setCredentialRepository(credentialRepo);
validator.initialize();
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
index 37403cf..123bb02 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
@@ -62,7 +62,7 @@ public class CredentialRegistrationSerializerTest extends AbstractWebAuthnTest {
user = UserIdentity.builder()
.name("jdoe")
.displayName("John Doe")
- .id(new ByteArray("userhandle".getBytes()))
+ .id(ByteArray.fromBase64(USER_HANDLE_B64))
.build();
final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
@@ -83,7 +83,7 @@ public class CredentialRegistrationSerializerTest extends AbstractWebAuthnTest {
.withTransports(new TreeSet<AuthenticatorTransport>())
.withRegistrationTime(Instant.now())
.withCredential(credential)
- .withAttestationMetadata(null)
+ .withAttestationMetadata(CollectionSupport.emptySet())
.withCredentialNickname("nickname")
.withDiscoverable(Optional.of(Boolean.TRUE))
.withUserVerified(true)
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
index abdf228..34746ed 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
@@ -45,6 +45,7 @@ import com.yubico.webauthn.data.UserIdentity;
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.storage.CredentialRegistration;
+import net.shibboleth.shared.collection.CollectionSupport;
/**
* Tests for {@link IdPStorageServiceCredentialRespository}.
@@ -103,7 +104,7 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
.withTransports(new TreeSet<AuthenticatorTransport>())
.withRegistrationTime(Instant.now())
.withCredential(credential)
- .withAttestationMetadata(null)
+ .withAttestationMetadata(CollectionSupport.emptySet())
.withCredentialNickname("nickname")
.withDiscoverable(Optional.of(Boolean.TRUE))
.withUserVerified(true)
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
new file mode 100644
index 0000000..31242ff
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
@@ -0,0 +1,170 @@
+// Copyright (c) 2018, Yubico AB
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+
+/**
+ * In memory credential repository to use for testing.
+ */
+public class InMemoryRegistrationStorage implements StorageServiceCredentialRepository {
+
+ private final Cache<String, Set<CredentialRegistration>> storage = CacheBuilder.newBuilder().maximumSize(1000)
+ .expireAfterAccess(1, TimeUnit.DAYS).build();
+
+ private static final Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class);
+
+ @Override
+ public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
+ return getRegistrationsByUsername(username).stream()
+ .map(registration -> PublicKeyCredentialDescriptor.builder()
+ .id(registration.getCredential().getCredentialId()).transports(registration.getTransports())
+ .build())
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
+ return getRegistrationsByUserHandle(userHandle).stream().findAny().map(CredentialRegistration::getUsername);
+ }
+
+ @Override
+ public Optional<ByteArray> getUserHandleForUsername(final String username) {
+ return getRegistrationsByUsername(username).stream().findAny().map(reg -> reg.getUserIdentity().getId());
+ }
+
+ @Override
+ public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
+ final Optional<CredentialRegistration> registrationMaybe = storage.asMap().values().stream()
+ .flatMap(Collection::stream)
+ .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId())).findAny();
+
+ logger.debug("lookup credential ID: {}, user handle: {}; result: {}", credentialId, userHandle,
+ registrationMaybe);
+ return registrationMaybe.map(registration -> RegisteredCredential.builder()
+ .credentialId(registration.getCredential().getCredentialId())
+ .userHandle(registration.getUserIdentity().getId())
+ .publicKeyCose(registration.getCredential().getPublicKeyCose())
+ .signatureCount(registration.getCredential().getSignatureCount()).build());
+ }
+
+ @Override
+ public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
+ return Collections.unmodifiableSet(storage.asMap().values().stream().flatMap(Collection::stream)
+ .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
+ .map(reg -> RegisteredCredential.builder().credentialId(reg.getCredential().getCredentialId())
+ .userHandle(reg.getUserIdentity().getId()).publicKeyCose(reg.getCredential().getPublicKeyCose())
+ .signatureCount(reg.getCredential().getSignatureCount()).build())
+ .collect(Collectors.toSet()));
+ }
+
+ public boolean addRegistrationByUsername(final String username, final CredentialRegistration reg) {
+ try {
+ return storage.get(username, HashSet::new).add(reg);
+ } catch (final ExecutionException e) {
+ logger.error("Failed to add registration", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Set<CredentialRegistration> getRegistrationsByUsername(final String username) {
+ try {
+ return storage.get(username, HashSet::new);
+ } catch (final ExecutionException e) {
+ logger.error("Registration lookup failed", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Collection<CredentialRegistration> getRegistrationsByUserHandle(final ByteArray userHandle) {
+ return storage.asMap().values().stream().flatMap(Collection::stream)
+ .filter(credentialRegistration -> userHandle.equals(credentialRegistration.getUserIdentity().getId()))
+ .collect(Collectors.toList());
+ }
+
+ public void updateSignatureCount(final AssertionResult result) {
+ final CredentialRegistration registration = getRegistrationByUsernameAndCredentialId(result.getUsername(),
+ result.getCredential().getCredentialId())
+ .orElseThrow(() -> new NoSuchElementException(
+ String.format("Credential \"%s\" is not registered to user \"%s\"",
+ result.getCredential().getCredentialId(), result.getUsername())));
+
+ final Set<CredentialRegistration> regs = storage.getIfPresent(result.getUsername());
+ regs.remove(registration);
+ regs.add(registration.withCredential(
+ registration.getCredential().toBuilder().signatureCount(result.getSignatureCount()).build()));
+ }
+
+ public Optional<CredentialRegistration> getRegistrationByUsernameAndCredentialId(final String username,
+ final ByteArray id) {
+ try {
+ return storage.get(username, HashSet::new).stream()
+ .filter(credReg -> id.equals(credReg.getCredential().getCredentialId())).findFirst();
+ } catch (final ExecutionException e) {
+ logger.error("Registration lookup failed", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean removeRegistrationByUsername(final String username,
+ final CredentialRegistration credentialRegistration) {
+ try {
+ return storage.get(username, HashSet::new).remove(credentialRegistration);
+ } catch (final ExecutionException e) {
+ logger.error("Failed to remove registration", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean removeAllRegistrations(final String username) {
+ storage.invalidate(username);
+ return true;
+ }
+
+ public boolean userExists(final String username) {
+ return !getRegistrationsByUsername(username).isEmpty();
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list