[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