[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