[java-idp-plugin-webauthn] branch main updated: Add existing username check in credential lookup from userHandle
Phil Smart
philip.smart at jisc.ac.uk
Wed May 15 12:48:01 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=25da3ac364902b1cb49b3d81066a28f38c7d3410
The following commit(s) were added to refs/heads/main by this push:
new 25da3ac Add existing username check in credential lookup from userHandle
25da3ac is described below
commit 25da3ac364902b1cb49b3d81066a28f38c7d3410
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed May 15 13:47:58 2024 +0100
Add existing username check in credential lookup from userHandle
- Add a check that ensures any potential username found from looking up
the user identity from the userHandle matches with any existing username
in the authentication context, e.g., from a passwordless username
collection step.
---
.../LookupRegisteredCredentialsFromUserHandle.java | 12 ++
...kupRegisteredCredentialsFromUserHandleTest.java | 237 +++++++++++++++++++++
2 files changed, 249 insertions(+)
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
index 3b608c7..57cee36 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
@@ -46,6 +46,9 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* An action that lookups existing registered credentials from the userHandle supplied in the authenticators assertion
* response. If no credentials exist and the trigger condition is set, an error event will be produced.
*
+ * <p>Also ensures the username found from the userHandle matches that in the authentication context if supplied e.g.
+ * for a passwordless authentication.</p>
+ *
* @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
* @post BaseWebAuthnContext.setExistingCredentials() is either null if no existing credentials are found, or contains
* the credentials from the credential repository
@@ -125,6 +128,7 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
+ final String username = context.getUsername();
final StorageServiceCredentialRepository repository = getCredentialRepository();
assert repository != null;
@@ -140,6 +144,13 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
log.debug("{} User could not be found from the supplied userHandle, no registered credentials",
getLogPrefix());
} else {
+ if (username != null && !potentialUsername.get().equals(username)) {
+ log.debug("{} Username '{}' found from the userHandle was not the same as in the authentication "
+ + "context '{}'",getLogPrefix(), potentialUsername.get(), username);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
final Collection<CredentialRegistration> credentials =
repository.getRegistrationsByUsername(potentialUsername.get());
if (credentials.isEmpty()) {
@@ -149,6 +160,7 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
credentialsFound = true;
}
+
}
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
new file mode 100644
index 0000000..5463365
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.assertNull;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+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.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.UserIdentity;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.WebAuthnAuthenticationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Tests for {@link LookupRegisteredCredentialsFromUserHandle}
+ */
+public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAuthnTest {
+
+ private LookupRegisteredCredentialsFromUserHandle lookup;
+
+ private UserIdentity userIdentity;
+
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+
+ lookup = new LookupRegisteredCredentialsFromUserHandle();
+ lookup.setWebAuthnClient(client);
+ lookup.setCredentialRepository(credentialRepo);
+ lookup.setTriggerEventOnNoCredentials(true);
+
+ userIdentity = UserIdentity.builder()
+ .name(USERNAME)
+ .displayName("Test User")
+ .id(ByteArray.fromBase64(USER_HANDLE_B64))
+ .build();
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testUserHandleHasCredentials() throws Exception {
+ lookup.initialize();
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(attestation.getId())
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialPublicKey())
+ .build();
+
+
+ final CredentialRegistration reg =CredentialRegistration.builder()
+ .withUserIdentity(userIdentity)
+ .withTransports(new TreeSet<AuthenticatorTransport>())
+ .withRegistrationTime(Instant.now())
+ .withCredential(credential)
+ .withAttestationMetadata(CollectionSupport.emptySet())
+ .withCredentialNickname("Nickname")
+ .withDiscoverable(Optional.of(Boolean.TRUE))
+ .withUserVerified(true)
+ .build();
+
+ credentialRepo.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 PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet);
+
+ webAuthnContext.setAuthenticatorAssertionResponse(assertion);
+
+ final Event event = lookup.execute(src);
+ assertNull(event);
+ }
+
+ /*
+ * The username should be used internally to extract the allow credentials list, so the userhandle should always
+ * match back to a credential this user has. So this should never really happen.
+ */
+ @SuppressWarnings("null")
+ @Test
+ public void testUserHandleHasCredentials_UsernameInContextIsDifferent() throws Exception {
+
+ // This is different from the username located from the userhandle
+ webAuthnContext.setUsername("username-collected");
+ lookup.initialize();
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(attestation.getId())
+ .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+ .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialPublicKey())
+ .build();
+
+
+ final CredentialRegistration reg =CredentialRegistration.builder()
+ .withUserIdentity(userIdentity)
+ .withTransports(new TreeSet<AuthenticatorTransport>())
+ .withRegistrationTime(Instant.now())
+ .withCredential(credential)
+ .withAttestationMetadata(CollectionSupport.emptySet())
+ .withCredentialNickname("Nickname")
+ .withDiscoverable(Optional.of(Boolean.TRUE))
+ .withUserVerified(true)
+ .build();
+
+ credentialRepo.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 PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet);
+
+ webAuthnContext.setAuthenticatorAssertionResponse(assertion);
+
+ final Event event = lookup.execute(src);
+ assertEquals(event.getId(), AuthnEventIds.NO_CREDENTIALS);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testUserHandleHasNoCredentials() throws Exception {
+ lookup.initialize();
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ // Do not register and store the credential attestation
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ // Now generate an assertion (authentication) and check it is valid
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet);
+
+ webAuthnContext.setAuthenticatorAssertionResponse(assertion);
+
+ final Event event = lookup.execute(src);
+ assertEquals(event.getId(), WebAuthnAuthenticationEventIds.NO_REGISTERED_WEBAUTHN_CREDENTIALS);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testUserHandleHasNoCredentialsNoTrigger() throws Exception {
+ lookup.setTriggerEventOnNoCredentials(false);
+ lookup.initialize();
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+ // Need to register a new credential first
+ final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation =
+ mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate,
+ Base64Support.decode(USER_HANDLE_B64));
+
+ // Do not register and store the credential attestation
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ // Now generate an assertion (authentication) and check it is valid
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet);
+
+ webAuthnContext.setAuthenticatorAssertionResponse(assertion);
+
+ final Event event = lookup.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