[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-32 - add a Last Used field to registration and management views

Phil Smart philip.smart at jisc.ac.uk
Mon Mar 3 15:38:50 UTC 2025


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=1bde07a177c87b226f2ef623a45e1ee86ac8c509

The following commit(s) were added to refs/heads/main by this push:
     new 1bde07a  JWEBAUTHN-32 - add a Last Used field to registration and management views
1bde07a is described below

commit 1bde07a177c87b226f2ef623a45e1ee86ac8c509
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Feb 20 09:21:18 2025 +0000

    JWEBAUTHN-32 - add a Last Used field to registration and management views
    
     - Add a new lastUsedTime field to the storage record
     - Add a new method (defaulted to maintain backward compat) to the
       credential repository to allow atomic updates of the last used
       field.
     - Added 'last used' time to the registration view and managment views
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-32
---
 .../authn/webauthn/storage/CredentialRecord.java   | 68 +++++++++++++++++++++-
 .../storage/WebAuthnCredentialRepository.java      | 17 ++++++
 .../webauthn/impl/ValidateWebAuthnAssertion.java   | 64 +++++++++++++++++++-
 .../IdPStorageServiceCredentialRespository.java    | 52 +++++++++++++++++
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  1 +
 .../authn/webauthn/conf/authn/webauthn.properties  |  3 +
 .../idp/plugin/authn/webauthn/messages.properties  |  2 +
 .../authn/webauthn/views/webauthn-management.vm    | 10 +++-
 .../authn/webauthn/views/webauthn-register.vm      |  9 ++-
 ...IdPStorageServiceCredentialRespositoryTest.java |  9 +--
 10 files changed, 222 insertions(+), 13 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRecord.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRecord.java
index 300f0ba..ae78f6d 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRecord.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRecord.java
@@ -40,7 +40,7 @@ import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 import net.shibboleth.shared.logic.Constraint;
 
 /**
- * A credential registration record used to hold registered credentials.
+ * A credential record used to hold a registered credential. Can be serialized in and out of storage.
  * 
  * <p>Equality is determined by comparing the wrapped {@link RegisteredCredential credential}.</p>
  */
@@ -79,6 +79,9 @@ public final class CredentialRecord {
     /** Was the user verified during registration. */
     private final boolean userVerified;
     
+    /** The time the credential was last used for authentication.*/
+    @Nullable private final Instant lastUsedTime;
+    
     /**
      * 
      * Builder constructor.
@@ -95,6 +98,7 @@ public final class CredentialRecord {
         this.discoverable = builder.discoverable;
         this.userVerified = builder.userVerified;
         this.aaguid = builder.aaguid;
+        this.lastUsedTime = builder.lastUsedTime;
     }
 
     /**
@@ -203,6 +207,16 @@ public final class CredentialRecord {
         return aaguid;
     }
     
+    /**
+     * Get the time the credential was last used for authentication
+     * 
+     * @return the last used time of the credential.
+     */
+    @JsonGetter("lastUsedTime")
+    public Instant getLastUsedTime() {
+        return lastUsedTime;
+    }
+    
     /**
      * Get the credential ID as a base64URL encoded string.
      * 
@@ -233,6 +247,16 @@ public final class CredentialRecord {
         return credential.getCredentialId().getHex();
     }
     
+    /**
+     * Get a builder based on this record which can be updated until it is finalised and built.
+     * 
+     * @return the builder
+     */
+    @JsonIgnore
+    public Builder toBuilder() {
+        return new Builder(this);
+    }
+    
     /**
      * Convert the credential registration into a {@link PublicKeyCredentialDescriptor}.
      * 
@@ -278,7 +302,10 @@ public final class CredentialRecord {
      * @param newRegisteredCred the new credential
      * 
      * @return a new {@link CredentialRecord} instance
+     * 
+     * @deprecated use {@link #toBuilder()}
      */
+    @Deprecated(since="1.1.0", forRemoval=true)
     @JsonIgnore
     public CredentialRecord withCredential(@Nonnull final RegisteredCredential newRegisteredCred) {
         return CredentialRecord.builder()
@@ -390,6 +417,14 @@ public final class CredentialRecord {
          * @return the next builder stage
          */
         @Nonnull public IBuildStage withAaguid(byte[] aaguid);
+        
+        /**
+         * Set the last authenticated time for this credential.
+         * 
+         * @param time the time the credential was last used to authenticate.
+         * @return the next builder stage
+         */
+        @Nonnull public IBuildStage withLastUsedTime(Instant time);
 
         /**
          * Build this credential registration.
@@ -421,15 +456,37 @@ public final class CredentialRecord {
         private boolean userVerified;        
         /** The AAGUID of the authenticator.*/
         @Nullable private byte[] aaguid;
+        /** The time the credential was last used for authentication.*/
+        @Nullable private Instant lastUsedTime;
 
         /** Constructor.*/
         @SuppressWarnings("null")
-        private Builder() {
+        public Builder() {
             // Create empty, corresponds to 'unknown'
             discoverable = Optional.empty();            
             userVerified = false;
             transports = Collections.emptySortedSet();
         }
+        
+        /**
+         * Constructor to build a new {@link CredentialRecord} from the given one. This remains mutable until this
+         * object is built.
+         *
+         * @param existingRecord the existing record to copy
+         */
+        //TODO should this deep copy? otherwise setters might be sufficient (i.e. add setters)
+        public Builder(final CredentialRecord existingRecord) {
+            userIdentity = existingRecord.userIdentity;
+            username = existingRecord.username;
+            transports = existingRecord.transports;
+            registrationTime = existingRecord.registrationTime;
+            credential = existingRecord.credential;
+            credentialNickname = existingRecord.credentialNickname;
+            discoverable = existingRecord.discoverable;
+            userVerified = existingRecord.userVerified;
+            aaguid = existingRecord.aaguid;
+            lastUsedTime = existingRecord.lastUsedTime;            
+        }
 
         @Override
         @JsonProperty("userIdentity")
@@ -491,6 +548,13 @@ public final class CredentialRecord {
             userVerified = isUserVerified;
             return this;
         }
+        
+        @Override
+        @JsonProperty("lastUsedTime")
+        @Nonnull public IBuildStage withLastUsedTime(final Instant time) {
+            lastUsedTime = time;
+            return this;
+        }
 
         @Override
         @Nonnull public CredentialRecord build() {
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
index ee0ec74..9ca5141 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
@@ -14,6 +14,7 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.storage;
 
+import java.time.Instant;
 import java.util.Optional;
 import java.util.Set;
 
@@ -135,5 +136,21 @@ public interface WebAuthnCredentialRepository extends CredentialRepository {
     boolean removeRegistrationByUsernameAndCredentialId(final @Nonnull String username, 
             final @Nonnull ByteArray credentialId);
     
+    /**
+     * Update the last used time for the credential that belongs to the given user.
+     * 
+     * @param username the username of the user to update the last used time for
+     * @param credentialId the identifier of the credential to update the last used time for
+     * @param lastUsedTime the new last used time value
+     * 
+     * @return true iff the registration was updated, false otherwise.
+     * 
+     * @since 1.1.0
+     */
+    //TODO remove default in v2.
+    default boolean updateLastUsedTime(final @Nonnull String username, final @Nonnull ByteArray credentialId, 
+           @Nonnull final Instant lastUsedTime) {
+        return false;
+    }    
     
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
index 250df3a..6863b40 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
@@ -14,6 +14,7 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
+import java.time.Instant;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -87,11 +88,15 @@ public class ValidateWebAuthnAssertion extends AbstractAuditingValidationAction
      */
     @Nonnull private Predicate<ProfileRequestContext> updateSignatureCount;
     
+    /** Should we updated the last used time on successful validation?. Defaults to false.*/
+    @Nonnull private Predicate<ProfileRequestContext> updateLastUsedTime;
+    
     /** Constructor. */
     public ValidateWebAuthnAssertion() {
         webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class)
                 .compose(new ChildContextLookup<>(AuthenticationContext.class));
         updateSignatureCount = PredicateSupport.alwaysTrue();
+        updateLastUsedTime = PredicateSupport.alwaysFalse();
 
     }
     
@@ -147,7 +152,19 @@ public class ValidateWebAuthnAssertion extends AbstractAuditingValidationAction
     public void setUpdateSignatureCountPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
         checkSetterPreconditions();
         updateSignatureCount = Constraint.isNotNull(predicate, "updateSignatureCount predicate can not be null");
-    }   
+    }
+    
+    /**
+     * Set the predicate to determine if we should update the last used time on the credential in the repository 
+     * after successful validation?
+     * 
+     * @param predicate the predicate to set..
+     */
+    public void setUpdateLastUsedTimePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        updateLastUsedTime = Constraint.isNotNull(predicate, "updateLastUsedTime can not be null");
+    }
+    
     
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@@ -203,6 +220,10 @@ public class ValidateWebAuthnAssertion extends AbstractAuditingValidationAction
                 // Update the signature count with that from the assertion. It has already been validated at this point
                 updateSignatureCount(result.getUsername(), assertion);
             }
+            // Now update last used time if enabled
+            if (updateLastUsedTime.test(profileRequestContext)) {
+                updateLastUsedTime(result.getUsername(), assertion);
+            }
             
             log.info("{} WebAuthn authentication succeeded for '{}', authenticator verified the user '{}'",
                     getLogPrefix(), result.getUsername(), result.isUserVerified());
@@ -223,7 +244,44 @@ public class ValidateWebAuthnAssertion extends AbstractAuditingValidationAction
         }
         
     }
-
+    
+    /**
+     * Update the last used time of the credential to {@link Instant#now()}. Any failure just gets logged and normal
+     * processing continues.
+     * 
+     * @param username the username of the user to update the last used time for
+     * @param assertion the assertion with the credential Id to update
+     * 
+     * @throws AssertionFailureException on error updating the counter
+     */
+    private void updateLastUsedTime(@Nonnull final String username, 
+          @Nonnull final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion) 
+                    throws AssertionFailureException {
+        
+        final ByteArray credentialId = assertion.getId();
+        if (credentialId == null) {
+            log.debug("{} Failed to updated last used time of credential for user '{}'; no credentialId in assertion", 
+                    getLogPrefix(), username);
+            return;
+        }
+        
+        try {
+            if (!credentialRepository.updateLastUsedTime(username, credentialId, Instant.now())) {
+                // This is not fatal.
+                log.debug("{} Failed to updated last used time of credential '{}' for user '{}'",
+                        getLogPrefix(), credentialId.getBase64() , username);
+            } else {
+                log.trace("{} Updated credential last used time for credential '{}' and user '{}'", getLogPrefix(), 
+                        credentialId.getBase64() , username);
+            }
+        } catch (final CredentialRepositoryException e) {
+            // This is not fatal.
+            log.debug("{} Failed to updated last used time of credential '{}' for user '{}'", getLogPrefix(), 
+                    credentialId.getBase64(), username, e);
+        }
+        
+    }
+    
     /**
      * Take the new signature count from the authenticator assertion response and update the stored credential with that
      * value. It is assumed the assertion and signature counter is valid by the time this method is called.
@@ -312,4 +370,4 @@ public class ValidateWebAuthnAssertion extends AbstractAuditingValidationAction
         
     }
 
-}
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
index 964e589..991aa8a 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -601,6 +602,57 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
             writeLock.unlock();
         } 
     }
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean updateLastUsedTime(@Nonnull final String username, @Nonnull final ByteArray credentialId, 
+            final Instant lastUsedTime) {
+        final Lock writeLock = lock.writeLock();
+        try {
+            writeLock.lock();
+            
+            final VersionedCredentialSet existingRegistrations = getRegistrationsByUsernameWithVersion(username);
+            final Optional<CredentialRecord> credential = existingRegistrations.getCredentials().stream()
+                    .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId()))
+                    .findFirst();
+            
+            if (credential.isEmpty()) {
+                log.warn("Can not update signature count for user '{}' and credential '{}'. "
+                        + "No existing credential found.", username, credentialId.getBase64());
+                return false;                
+            }
+
+            // Copy the credential into a new credential and update the last used time
+            final CredentialRecord updatedRegistration = credential.get().toBuilder()
+                    .withLastUsedTime(lastUsedTime)
+                    .build();
+            
+            final Set<CredentialRecord> updateCredentialSet = 
+                    new LinkedHashSet<>(existingRegistrations.getCredentials());
+            updateCredentialSet.remove(credential.get());
+            updateCredentialSet.add(updatedRegistration);
+            
+            if (updateCredentialSet.isEmpty()) {
+                // We are updating, so this should not be possible
+                log.debug("Can not update signature count for user '{}' and credential '{}'. "
+                        + "Update set is empty.", username, credentialId.getBase64());
+                return false;
+            }
+
+            try {
+                final Long updatedVersion = storageService.updateWithVersion(
+                        existingRegistrations.getVersion(), STORAGE_CONTEXT, username, updateCredentialSet, 
+                        serializer, null);                
+                return updatedVersion != null;
+            } catch (IOException | VersionMismatchException e) {
+                throw new CredentialRepositoryException(e);    
+            } 
+            
+        } finally {
+            writeLock.unlock();
+        } 
+        
+    }
 
     /** {@inheritDoc} */
     @Override
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 10c16eb..c7721d2 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -187,6 +187,7 @@
         p:webAuthnClient="#{getObject('shibboleth.authn.WebAuthn.WebAuthnAuthenticationClientFactory') ?: getObject('shibboleth.authn.WebAuthn.DefaultWebAuthnAuthenticationClientFactory')}" 
         p:credentialRepository="#{getObject('shibboleth.authn.WebAuthn.CredentialRepository') ?: getObject('shibboleth.authn.WebAuthn.DefaultCredentialRepository')}"
         p:updateSignatureCountPredicate="#{getObject('shibboleth.authn.WebAuthn.UpdateSignatureCountPredicate') ?: %{idp.authn.webauthn.updateSignatureCount:true}}"
+        p:updateLastUsedTimePredicate="#{getObject('shibboleth.authn.WebAuthn.UpdateLastUsedTimePredicate') ?: %{idp.authn.webauthn.updateLastUsedTime:false}}"    
         p:populateAuditContextAction="#{%{idp.authn.webauthn.audit.enabled:%{idp.authn.audit.enabled:false}} ? getObject('shibboleth.authn.WebAuthn.PopulateAuditContext') : null}"
         p:writeAuditLogAction="#{%{idp.authn.webauthn.audit.enabled:%{idp.authn.audit.enabled:false}} ? getObject('WriteAuthnAuditLog') : null}"
         p:cleanupHook="#{getObject('shibboleth.authn.WebAuthn.RemoveAfterValidation') == true ? getObject('DefaultCleanupHook') : null}"        
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
index 9ba3c34..0a05e7b 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
@@ -51,6 +51,9 @@ idp.authn.webauthn.supportedPrincipals = \
 # Should we update an authenticators signature counter inside the credential repository after each successful authentication?  
 #idp.authn.webauthn.updateSignatureCount = true
 
+# Should the last authentication time for a credential be updated after each successful authentication?  
+#idp.authn.webauthn.updateLastUsedTime = false
+
 # Should an event be built if there are no credentials found? Only applicable to passwordless authentication.
 #idp.authn.webauthn.passwordless.signalEventOnNoCredentials = false
 #idp.authn.webauthn.passwordless.noCredentialsEventId = NoRegisteredWebAuthnCredentials
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
index 99db804..d70e5a0 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
@@ -26,6 +26,7 @@ idp.webauthn.register.table.header.authenticatorDescription = Authenticator
 idp.webauthn.register.table.header.authenticatorIcon = Icon
 idp.webauthn.register.table.header.labels = Labels
 idp.webauthn.register.table.header.registrationTime = Registration Time
+idp.webauthn.register.table.header.lastUsedTime = Last Used
 idp.webauthn.register.table.header.action = Action
 idp.webauthn.register.table.unknownCredential = unknown
 idp.webauthn.register.credential.remove = Remove
@@ -45,6 +46,7 @@ idp.webauthn.admin.table.header.keyName = Key Name
 idp.webauthn.admin.table.header.authenticatorDescription = Authenticator
 idp.webauthn.admin.table.header.authenticatorIcon = Icon
 idp.webauthn.admin.table.header.registrationTime = Registration Time
+idp.webauthn.admin.table.header.lastUsedTime = Last Used
 idp.webauthn.admin.table.header.action = Action
 idp.webauthn.admin.noKeys  = There are no registered keys
 idp.webauthn.admin.unsupported = Your browser is not WebAuthn compatible
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
index 779921f..4dfb1a2 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
@@ -17,6 +17,8 @@
 ## adminInfoMessageFunction - function to return info admin messages
 ## adminErrorMessageFunction - function to return error admin messages
 
+#set ($lastUsed = $environment.getProperty("idp.authn.webauthn.updateLastUsedTime", "false"))
+
 ## Add CSP directives
 #set ($areYouSure =  "return confirm('#springMessageText('idp.webauthn.register.credential.remove.confirm', 'Are you sure')');")
 #set ($nonce = $cspNonce.generateIdentifier())
@@ -62,7 +64,11 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                <th>#springMessageText("idp.webauthn.admin.table.header.authenticatorDescription", "Authenticator")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.authenticatorIcon", "Icon")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.labels", "Labels")</th>
-                               <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
+                               #if ($lastUsed == 'true')
+                                <th>#springMessageText("idp.webauthn.admin.table.header.lastUsedTime", "Last Used")</th>
+                               #else
+                                <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
+                               #end
                                <th>#springMessageText("idp.webauthn.admin.table.header.hasMetadata", "Metadata?")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.action", "Action")</th>
                             </tr>
@@ -84,7 +90,7 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                         <span class="label info">$encoder.encodeForHTML($label)</span>
                                     #end  
                                    </td>
-                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.credentialRecord.registrationTime))</td>
+                                   <td>$encoder.encodeForHTML($webAuthnEncoder.chooseLatestAndTransform($cred.credentialRecord.lastUsedTime, $cred.credentialRecord.registrationTime))</td>
                                    <td>
                                         #if ($webAuthnEncoder.isAuthenticatorMetadataAttached($cred))
                                            #springMessageText("idp.webauthn.admin.table.hasMetadata", "Yes") 
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
index 202ea57..0e1d062 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -18,6 +18,7 @@
 ## registrationErrorMessageFunction - function to produce error message for form
 ##
 #set ($debug = $environment.getProperty("idp.authn.webauthn.ui.debug", "false"))
+#set ($lastUsed = $environment.getProperty("idp.authn.webauthn.updateLastUsedTime", "false"))
 
 ## Add CSP directives
 #set ($areYouSure =  "return confirm('#springMessageText('idp.webauthn.register.credential.remove.confirm', 'Are you sure')');")
@@ -132,7 +133,11 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                <th>#springMessageText("idp.webauthn.register.table.header.authenticatorDescription", "Authenticator")</th>
                                <th>#springMessageText("idp.webauthn.register.table.header.authenticatorIcon", "Icon")</th> 
                                <th>#springMessageText("idp.webauthn.register.table.header.labels", "Labels")</th>
-                               <th>#springMessageText("idp.webauthn.register.table.header.registrationTime", "Registration Time")</th>
+                               #if ($lastUsed == 'true')
+                                <th>#springMessageText("idp.webauthn.register.table.header.lastUsedTime", "Last Used")</th>
+                               #else
+                                <th>#springMessageText("idp.webauthn.register.table.header.registrationTime", "Registration Time")</th>
+                               #end
                                <th>#springMessageText("idp.webauthn.register.table.header.action", "Action")</th>
                             </tr>
                             #foreach($cred in $webauthnRegContext.existingCredentials)
@@ -153,7 +158,7 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                     <span class="label info">$encoder.encodeForHTML($label)</span>
                                 #end  
                                </td>                             
-                               <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.credentialRecord.registrationTime))</td>
+                               <td>$encoder.encodeForHTML($webAuthnEncoder.chooseLatestAndTransform($cred.credentialRecord.lastUsedTime, $cred.credentialRecord.registrationTime))</td>
                                <td>
                                   <form id="delete_key_form" action="$flowExecutionUrl" method="post">
                                      #parse("csrf/csrf.vm")
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 85e7c40..90549bc 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
@@ -813,8 +813,10 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
         final ExecutorService service = Executors.newFixedThreadPool(3);
         final Collection<Future<Boolean>> futures = new ArrayList<>(3);
         
-        futures.add(service.submit(()->repo.updateSignatureCounter(
-                "jdoe", registration.getCredential().getCredentialId(), 2)));
+        final var now = Instant.now();
+        
+        futures.add(service.submit(()->repo.updateLastUsedTime(
+                "jdoe", registration.getCredential().getCredentialId(),now)));
         futures.add(service.submit(()->{
             final var cred = repo.getRegistrationByUsernameAndCredentialId(
                     "jdoe", registration.getCredential().getCredentialId());
@@ -822,8 +824,7 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
             // But not yet added back to the updated credential
             if (cred.isEmpty()) return false;
             // Accept this read either happens before the update or after. 
-            return cred.get().getCredential().getSignatureCount() == 2 || 
-                    cred.get().getCredential().getSignatureCount() == 0;
+            return cred.get().getLastUsedTime().equals(now);
         }));
 
         for (final Future<Boolean> f : futures) {

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list