This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 7d75c9f958 [SYNCOPE-1772] Adding MfaTrustedDevice to AuhtProfile 
(#502) (#503)
7d75c9f958 is described below

commit 7d75c9f958f106b3efb665db896b8504f6792913
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Sat Aug 5 08:57:35 2023 +0200

    [SYNCOPE-1772] Adding MfaTrustedDevice to AuhtProfile (#502) (#503)
---
 .../authprofiles/AuthProfileDirectoryPanel.java    |  61 ++++++++
 .../client/console/commons/AMConstants.java        |   6 +-
 .../AuthProfileDirectoryPanel.properties           |   5 +
 .../AuthProfileDirectoryPanel_fr_CA.properties     |   5 +
 .../AuthProfileDirectoryPanel_it.properties        |   5 +
 .../AuthProfileDirectoryPanel_ja.properties        |   5 +
 .../AuthProfileDirectoryPanel_pt_BR.properties     |   5 +
 .../AuthProfileDirectoryPanel_ru.properties        |   5 +
 .../apache/syncope/client/ui/commons/DateOps.java  |  48 +++++-
 .../syncope/client/console/panels/BeanPanel.java   |   6 +-
 .../client/console/rest/ReportRestClient.java      |   2 +-
 .../client/console/rest/TaskRestClient.java        |   2 +-
 .../repeater/data/table/DatePropertyColumn.java    |   3 +
 .../console/wizards/DelegationWizardBuilder.java   |   4 +-
 .../syncope/common/lib/to/AuthProfileTO.java       |  26 ++++
 .../syncope/common/lib/wa/MfaTrustedDevice.java    | 136 +++++++++++++++++
 .../rest/api/beans/MfaTrustedDeviceQuery.java      | 128 ++++++++++++++++
 .../api/service/wa/MfaTrustStorageService.java     |  62 ++++++++
 .../apache/syncope/core/logic/AMLogicContext.java  |  11 ++
 ...strationLogic.java => MfaTrusStorageLogic.java} | 116 ++++++++-------
 .../core/logic/wa/U2FRegistrationLogic.java        |   5 +-
 .../syncope/core/rest/cxf/AMRESTCXFContext.java    |   9 ++
 .../cxf/service/wa/MfaTrustStorageServiceImpl.java |  61 ++++++++
 .../persistence/api/entity/am/AuthProfile.java     |   5 +
 .../persistence/jpa/entity/am/JPAAuthProfile.java  |  23 ++-
 .../java/data/AuthProfileDataBinderImpl.java       |   2 +
 .../console/panels/SCIMConfGeneralPanel.java       |   4 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   4 +
 .../syncope/fit/core/wa/MfaTrustStorageTCase.java  |  92 ++++++++++++
 pom.xml                                            |   6 +-
 wa/starter/pom.xml                                 |   8 +
 .../syncope/wa/starter/config/WAContext.java       |  21 +++
 .../WAMultifactorAuthenticationTrustStorage.java   | 165 +++++++++++++++++++++
 .../WAGoogleMfaAuthTokenRepositoryTest.java        |   2 +-
 34 files changed, 970 insertions(+), 78 deletions(-)

diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
index 80f2dfc9ae..f8f86b7034 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
@@ -45,6 +45,7 @@ import org.apache.syncope.common.lib.types.AMEntitlement;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
 import org.apache.syncope.common.lib.wa.U2FDevice;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.wicket.PageReference;
@@ -157,6 +158,15 @@ public class AuthProfileDirectoryPanel
                 return 
!rowModel.getObject().getU2FRegisteredDevices().isEmpty();
             }
         });
+        columns.add(new BooleanConditionColumn<>(new 
StringResourceModel("mfaTrustedDevices")) {
+
+            private static final long serialVersionUID = -8236820422411536323L;
+
+            @Override
+            protected boolean isCondition(final IModel<AuthProfileTO> 
rowModel) {
+                return !rowModel.getObject().getMfaTrustedDevices().isEmpty();
+            }
+        });
         columns.add(new BooleanConditionColumn<>(new 
StringResourceModel("webAuthnAccount")) {
 
             private static final long serialVersionUID = -8236820422411536323L;
@@ -368,6 +378,57 @@ public class AuthProfileDirectoryPanel
             }
         }, ActionLink.ActionType.FO_EDIT, AMEntitlement.AUTH_PROFILE_UPDATE);
 
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
AuthProfileTO ignore) {
+                model.setObject(restClient.read(model.getObject().getKey()));
+                target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
+                        authProfileModal,
+                        new AuthProfileItemDirectoryPanel<MfaTrustedDevice>(
+                                "panel", restClient, authProfileModal, 
model.getObject(), pageRef) {
+
+                    private static final long serialVersionUID = 
5788448799796630011L;
+
+                    @Override
+                    protected List<MfaTrustedDevice> getItems() {
+                        return model.getObject().getMfaTrustedDevices();
+                    }
+
+                    @Override
+                    protected MfaTrustedDevice defaultItem() {
+                        return new MfaTrustedDevice();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "id";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return 
AMConstants.PREF_AUTHPROFILE_MFA_TRUSTED_FDEVICES_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<MfaTrustedDevice, String>> 
getColumns() {
+                        List<IColumn<MfaTrustedDevice, String>> columns = new 
ArrayList<>();
+                        columns.add(new PropertyColumn<>(new 
ResourceModel("id"), "id", "id"));
+                        columns.add(new PropertyColumn<>(new 
ResourceModel("name"), "name", "name"));
+                        columns.add(new DatePropertyColumn<>(
+                                new ResourceModel("recordDate"), "recordDate", 
"recordDate"));
+                        columns.add(new DatePropertyColumn<>(
+                                new ResourceModel("expirationDate"), 
"expirationDate", "expirationDate"));
+                        return columns;
+                    }
+                }, pageRef)));
+                authProfileModal.header(new 
Model<>(getString("mfaTrustedDevices", model)));
+                authProfileModal.show(true);
+            }
+        }, ActionLink.ActionType.DOWN, AMEntitlement.AUTH_PROFILE_UPDATE);
+
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
index c185b55d1a..08c9663fb5 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
@@ -48,7 +48,11 @@ public final class AMConstants {
     public static final String 
PREF_AUTHPROFILE_GOOGLEMFAAUTHACCOUNTS_PAGINATOR_ROWS =
             "authprofile.googlemfaauthaccounts.paginator.rows";
 
-    public static final String PREF_AUTHPROFILE_U2FDEVICES_PAGINATOR_ROWS = 
"authprofile.u2fdevices.paginator.rows";
+    public static final String PREF_AUTHPROFILE_U2FDEVICES_PAGINATOR_ROWS =
+            "authprofile.u2fdevices.paginator.rows";
+
+    public static final String 
PREF_AUTHPROFILE_MFA_TRUSTED_FDEVICES_PAGINATOR_ROWS =
+            "authprofile.mfaTrustedDevices.paginator.rows";
 
     public static final String 
PREF_AUTHPROFILE_WEBAUTHNDEVICECREDENTIALS_PAGINATOR_ROWS =
             "authprofile.webAuthnDeviceCredentials.paginator.rows";
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
index e095d91b2b..56627ae0a4 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
@@ -44,3 +44,8 @@ json=JSON
 html.class=fas fa-at
 html.title=webauthn
 webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
index 8a42e53c73..495d278dbc 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
@@ -44,3 +44,8 @@ json=JSON
 html.class=fas fa-at
 html.title=webauthn
 webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
index 0fd02d5f98..cc0d9a5ce7 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
@@ -44,3 +44,8 @@ json=JSON
 html.class=fas fa-at
 html.title=webauthn
 webAuthnDeviceCredentials=Dispositivi Credenziali WebAuthn
+expirationDate=Scadenza
+recordDate=Memorizzazione
+mfaTrustedDevices=Dispositivi MFA
+down.title=dispositivi mfa
+down.class=fas fa-barcode
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
index d2fc5697f7..f3d1ac236c 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
@@ -44,3 +44,8 @@ json=JSON
 html.class=fas fa-at
 html.title=webauthn
 webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
index 8d919ed952..e227e6f900 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
@@ -44,3 +44,8 @@ json=JSON
 html.class=fas fa-at
 html.title=webauthn
 webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
index b638d451d5..a60b6d6825 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
@@ -45,3 +45,8 @@ json=JSON
 html.class=fas fa-at
 html.title=webauthn
 webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
diff --git 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
index f68187b4cb..17216b409b 100644
--- 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
+++ 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
@@ -20,7 +20,9 @@ package org.apache.syncope.client.ui.commons;
 
 import java.io.Serializable;
 import java.time.OffsetDateTime;
+import java.time.ZoneId;
 import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.Date;
 import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
@@ -46,39 +48,71 @@ public final class DateOps {
         public String format(final OffsetDateTime date) {
             return Optional.ofNullable(date).map(v -> 
fdf.format(convert(date))).orElse(StringUtils.EMPTY);
         }
+
+        public String format(final ZonedDateTime date) {
+            return Optional.ofNullable(date).map(v -> 
fdf.format(convert(date))).orElse(StringUtils.EMPTY);
+        }
     }
 
-    public static class WrappedDateModel implements IModel<Date>, Serializable 
{
+    public static final class WrappedDateModel implements IModel<Date>, 
Serializable {
 
         private static final long serialVersionUID = 31027882183172L;
 
-        private final IModel<OffsetDateTime> wrapped;
+        public static WrappedDateModel ofOffset(final IModel<OffsetDateTime> 
offset) {
+            WrappedDateModel instance = new WrappedDateModel();
+            instance.offset = offset;
+            return instance;
+        }
+
+        public static WrappedDateModel ofZoned(final IModel<ZonedDateTime> 
zoned) {
+            WrappedDateModel instance = new WrappedDateModel();
+            instance.zoned = zoned;
+            return instance;
+        }
+
+        private IModel<OffsetDateTime> offset;
 
-        public WrappedDateModel(final IModel<OffsetDateTime> wrapped) {
-            this.wrapped = wrapped;
+        private IModel<ZonedDateTime> zoned;
+
+        private WrappedDateModel() {
+            // private constructor for static utility class
         }
 
         @Override
         public Date getObject() {
-            return convert(wrapped.getObject());
+            return offset == null ? convert(zoned.getObject()) : 
convert(offset.getObject());
         }
 
         @Override
         public void setObject(final Date object) {
-            wrapped.setObject(convert(object));
+            if (offset == null) {
+                zoned.setObject(toZonedDateTime(object));
+            } else {
+                offset.setObject(toOffsetDateTime(object));
+            }
         }
     }
 
     public static final ZoneOffset DEFAULT_OFFSET = 
OffsetDateTime.now().getOffset();
 
+    public static final ZoneId DEFAULT_ZONE = ZonedDateTime.now().getZone();
+
     public static Date convert(final OffsetDateTime date) {
         return Optional.ofNullable(date).map(v -> new 
Date(v.toInstant().toEpochMilli())).orElse(null);
     }
 
-    public static OffsetDateTime convert(final Date date) {
+    public static Date convert(final ZonedDateTime date) {
+        return Optional.ofNullable(date).map(v -> new 
Date(v.toInstant().toEpochMilli())).orElse(null);
+    }
+
+    public static OffsetDateTime toOffsetDateTime(final Date date) {
         return Optional.ofNullable(date).map(v -> 
v.toInstant().atOffset(DEFAULT_OFFSET)).orElse(null);
     }
 
+    public static ZonedDateTime toZonedDateTime(final Date date) {
+        return Optional.ofNullable(date).map(v -> 
ZonedDateTime.ofInstant(v.toInstant(), DEFAULT_ZONE)).orElse(null);
+    }
+
     private DateOps() {
         // private constructor for static utility class
     }
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
index 7ee7b5e73b..aef0071628 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
@@ -27,6 +27,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.ParameterizedType;
 import java.time.Duration;
 import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -301,7 +302,10 @@ public class BeanPanel<T extends Serializable> extends 
Panel {
             panel = new AjaxDateTimeFieldPanel(id, fieldName, model,
                     
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
         } else if (OffsetDateTime.class.equals(type)) {
-            panel = new AjaxDateTimeFieldPanel(id, fieldName, new 
DateOps.WrappedDateModel(model),
+            panel = new AjaxDateTimeFieldPanel(id, fieldName, 
DateOps.WrappedDateModel.ofOffset(model),
+                    
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
+        } else if (ZonedDateTime.class.equals(type)) {
+            panel = new AjaxDateTimeFieldPanel(id, fieldName, 
DateOps.WrappedDateModel.ofZoned(model),
                     
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
         } else if (type.isEnum()) {
             panel = new AjaxDropDownChoicePanel(id, fieldName, model).
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
index 4345deac6d..7d7b6449e6 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
@@ -85,7 +85,7 @@ public class ReportRestClient extends BaseRestClient 
implements ExecutionRestCli
     @Override
     public void startExecution(final String reportKey, final Date startAt) {
         getService(ReportService.class).execute(new 
ExecSpecs.Builder().key(reportKey).
-                startAt(DateOps.convert(startAt)).build());
+                startAt(DateOps.toOffsetDateTime(startAt)).build());
     }
 
     @Override
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
index e4b6dc67f3..1a9b50014b 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
@@ -203,7 +203,7 @@ public class TaskRestClient extends BaseRestClient 
implements ExecutionRestClien
 
     public void startExecution(final String taskKey, final Date startAt, final 
boolean dryRun) {
         getService(TaskService.class).execute(new 
ExecSpecs.Builder().key(taskKey).
-                startAt(DateOps.convert(startAt)).dryRun(dryRun).build());
+                
startAt(DateOps.toOffsetDateTime(startAt)).dryRun(dryRun).build());
     }
 
     @Override
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
index 83c1e68cbd..bdc0674ffe 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
@@ -19,6 +19,7 @@
 package 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table;
 
 import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
 import java.util.Date;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import 
org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
@@ -49,6 +50,8 @@ public class DatePropertyColumn<T> extends PropertyColumn<T, 
String> {
         String convertedDate = "";
         if (date.getObject() instanceof OffsetDateTime) {
             convertedDate = 
SyncopeConsoleSession.get().getDateFormat().format((OffsetDateTime) 
date.getObject());
+        } else if (date.getObject() instanceof ZonedDateTime) {
+            convertedDate = 
SyncopeConsoleSession.get().getDateFormat().format((ZonedDateTime) 
date.getObject());
         } else if (date.getObject() instanceof Date) {
             convertedDate = 
SyncopeConsoleSession.get().getDateFormat().format((Date) date.getObject());
         }
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
index c284bfba6f..675f2ecf42 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
@@ -151,14 +151,14 @@ public class DelegationWizardBuilder extends 
BaseAjaxWizardBuilder<DelegationTO>
             add(new AjaxDateTimeFieldPanel(
                     "start",
                     "start",
-                    new DateOps.WrappedDateModel(new 
PropertyModel<>(modelObject, "start")),
+                    DateOps.WrappedDateModel.ofOffset(new 
PropertyModel<>(modelObject, "start")),
                     
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT).
                     addRequiredLabel());
 
             add(new AjaxDateTimeFieldPanel(
                     "end",
                     "end",
-                    new DateOps.WrappedDateModel(new 
PropertyModel<>(modelObject, "end")),
+                    DateOps.WrappedDateModel.ofOffset(new 
PropertyModel<>(modelObject, "end")),
                     
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT));
         }
     }
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
index 7cd493db5c..f06cde8cc8 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
@@ -29,6 +29,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
 import org.apache.syncope.common.lib.wa.U2FDevice;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 
@@ -95,6 +96,21 @@ public class AuthProfileTO implements EntityTO {
             return this;
         }
 
+        public AuthProfileTO.Builder mfaTrustedDevice(final MfaTrustedDevice 
device) {
+            instance.getMfaTrustedDevices().add(device);
+            return this;
+        }
+
+        public AuthProfileTO.Builder mfaTrustedDevices(final 
MfaTrustedDevice... devices) {
+            instance.getMfaTrustedDevices().addAll(List.of(devices));
+            return this;
+        }
+
+        public AuthProfileTO.Builder mfaTrustedDevices(final 
Collection<MfaTrustedDevice> devices) {
+            instance.getMfaTrustedDevices().addAll(devices);
+            return this;
+        }
+
         public AuthProfileTO.Builder credential(final WebAuthnDeviceCredential 
credential) {
             instance.getWebAuthnDeviceCredentials().add(credential);
             return this;
@@ -127,6 +143,8 @@ public class AuthProfileTO implements EntityTO {
 
     private final List<U2FDevice> u2fRegisteredDevices = new ArrayList<>();
 
+    private final List<MfaTrustedDevice> mfaTrustedDevices = new ArrayList<>();
+
     private final List<WebAuthnDeviceCredential> webAuthnDeviceCredentials = 
new ArrayList<>();
 
     @Override
@@ -172,6 +190,12 @@ public class AuthProfileTO implements EntityTO {
         return u2fRegisteredDevices;
     }
 
+    @JacksonXmlElementWrapper(localName = "mfaTrustedDevices")
+    @JacksonXmlProperty(localName = "mfaTrustedDevice")
+    public List<MfaTrustedDevice> getMfaTrustedDevices() {
+        return mfaTrustedDevices;
+    }
+
     @JacksonXmlElementWrapper(localName = "credentials")
     @JacksonXmlProperty(localName = "credential")
     public List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials() {
@@ -187,6 +211,7 @@ public class AuthProfileTO implements EntityTO {
                 append(googleMfaAuthTokens).
                 append(googleMfaAuthAccounts).
                 append(u2fRegisteredDevices).
+                append(mfaTrustedDevices).
                 append(webAuthnDeviceCredentials).
                 build();
     }
@@ -210,6 +235,7 @@ public class AuthProfileTO implements EntityTO {
                 append(googleMfaAuthTokens, other.googleMfaAuthTokens).
                 append(googleMfaAuthAccounts, other.googleMfaAuthAccounts).
                 append(u2fRegisteredDevices, other.u2fRegisteredDevices).
+                append(mfaTrustedDevices, other.mfaTrustedDevices).
                 append(webAuthnDeviceCredentials, 
other.webAuthnDeviceCredentials).
                 build();
     }
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/MfaTrustedDevice.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/MfaTrustedDevice.java
new file mode 100644
index 0000000000..b53efdd2e0
--- /dev/null
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/MfaTrustedDevice.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.syncope.common.lib.wa;
+
+import java.time.ZonedDateTime;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+
+public class MfaTrustedDevice implements BaseBean {
+
+    private static final long serialVersionUID = 5120423450725182470L;
+
+    private long id;
+
+    private String name;
+
+    private String deviceFingerprint;
+
+    private String recordKey;
+
+    private ZonedDateTime recordDate;
+
+    private ZonedDateTime expirationDate;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(final long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public String getDeviceFingerprint() {
+        return deviceFingerprint;
+    }
+
+    public void setDeviceFingerprint(final String deviceFingerprint) {
+        this.deviceFingerprint = deviceFingerprint;
+    }
+
+    public ZonedDateTime getRecordDate() {
+        return recordDate;
+    }
+
+    public void setRecordDate(final ZonedDateTime recordDate) {
+        this.recordDate = recordDate;
+    }
+
+    public String getRecordKey() {
+        return recordKey;
+    }
+
+    public void setRecordKey(final String recordKey) {
+        this.recordKey = recordKey;
+    }
+
+    public ZonedDateTime getExpirationDate() {
+        return expirationDate;
+    }
+
+    public void setExpirationDate(final ZonedDateTime expirationDate) {
+        this.expirationDate = expirationDate;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+                .append(id)
+                .append(name)
+                .append(deviceFingerprint)
+                .append(recordDate)
+                .append(recordKey)
+                .append(expirationDate)
+                .toHashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        MfaTrustedDevice other = (MfaTrustedDevice) obj;
+        return new EqualsBuilder()
+                .append(this.id, other.id)
+                .append(this.name, other.name)
+                .append(this.deviceFingerprint, other.deviceFingerprint)
+                .append(this.recordDate, other.recordDate)
+                .append(this.recordKey, other.recordKey)
+                .append(this.expirationDate, other.expirationDate)
+                .isEquals();
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append("id", id)
+                .append("name", name)
+                .append("deviceFingerprint", deviceFingerprint)
+                .append("recordDate", recordDate)
+                .append("recordKey", recordKey)
+                .append("expirationDate", expirationDate)
+                .toString();
+    }
+}
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/MfaTrustedDeviceQuery.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/MfaTrustedDeviceQuery.java
new file mode 100644
index 0000000000..69d4ae7a18
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/MfaTrustedDeviceQuery.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.syncope.common.rest.api.beans;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.ws.rs.QueryParam;
+import java.time.OffsetDateTime;
+
+public class MfaTrustedDeviceQuery extends AbstractQuery {
+
+    private static final long serialVersionUID = -7381828286332101171L;
+
+    public static class Builder extends 
AbstractQuery.Builder<MfaTrustedDeviceQuery, MfaTrustedDeviceQuery.Builder> {
+
+        @Override
+        protected MfaTrustedDeviceQuery newInstance() {
+            return new MfaTrustedDeviceQuery();
+        }
+
+        public MfaTrustedDeviceQuery.Builder id(final Long id) {
+            getInstance().setId(id);
+            return this;
+        }
+
+        public MfaTrustedDeviceQuery.Builder recordKey(final String recordKey) 
{
+            getInstance().setRecordKey(recordKey);
+            return this;
+        }
+
+        public MfaTrustedDeviceQuery.Builder principal(final String principal) 
{
+            getInstance().setPrincipal(principal);
+            return this;
+        }
+
+        public MfaTrustedDeviceQuery.Builder expirationDate(final 
OffsetDateTime date) {
+            getInstance().setExpirationDate(date);
+            return this;
+        }
+
+        public MfaTrustedDeviceQuery.Builder recordDate(final OffsetDateTime 
date) {
+            getInstance().setRecordDate(date);
+            return this;
+        }
+    }
+
+    private Long id;
+
+    private String recordKey;
+
+    private OffsetDateTime expirationDate;
+
+    private OffsetDateTime recordDate;
+
+    private String principal;
+
+    @Parameter(name = "id", in = ParameterIn.QUERY, schema =
+            @Schema(implementation = Long.class))
+    public Long getId() {
+        return id;
+    }
+
+    @QueryParam("id")
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    @Parameter(name = "recordKey", in = ParameterIn.QUERY, schema =
+            @Schema(implementation = String.class))
+    public String getRecordKey() {
+        return recordKey;
+    }
+
+    @QueryParam("recordKey")
+    public void setRecordKey(final String recordKey) {
+        this.recordKey = recordKey;
+    }
+
+    @Parameter(name = "expirationDate", in = ParameterIn.QUERY, schema =
+            @Schema(implementation = OffsetDateTime.class))
+    public OffsetDateTime getExpirationDate() {
+        return expirationDate;
+    }
+
+    @QueryParam("expirationDate")
+    public void setExpirationDate(final OffsetDateTime expirationDate) {
+        this.expirationDate = expirationDate;
+    }
+
+    @Parameter(name = "recordDate", in = ParameterIn.QUERY, schema =
+            @Schema(implementation = OffsetDateTime.class))
+    public OffsetDateTime getRecordDate() {
+        return recordDate;
+    }
+
+    @QueryParam("recordDate")
+    public void setRecordDate(final OffsetDateTime recordDate) {
+        this.recordDate = recordDate;
+    }
+
+    @Parameter(name = "principal", in = ParameterIn.QUERY, schema =
+            @Schema(implementation = String.class))
+    public String getPrincipal() {
+        return principal;
+    }
+
+    @QueryParam("principal")
+    public void setPrincipal(final String principal) {
+        this.principal = principal;
+    }
+}
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/MfaTrustStorageService.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/MfaTrustStorageService.java
new file mode 100644
index 0000000000..1c7c362d0a
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/MfaTrustStorageService.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.syncope.common.rest.api.service.wa;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import jakarta.ws.rs.BeanParam;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+@Tag(name = "WA")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("wa/mfaTrustedDevice")
+public interface MfaTrustStorageService extends JAXRSService {
+
+    @GET
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    PagedResult<MfaTrustedDevice> search(@BeanParam MfaTrustedDeviceQuery 
query);
+
+    @POST
+    @Path("{principal}")
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    void create(@NotNull @PathParam("principal") String principal, @NotNull 
MfaTrustedDevice device);
+
+    @DELETE
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    void delete(@BeanParam MfaTrustedDeviceQuery query);
+}
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
index 5535b72538..8a365cc522 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
@@ -23,6 +23,7 @@ import org.apache.syncope.core.logic.init.AMEntitlementLoader;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic;
 import org.apache.syncope.core.logic.wa.ImpersonationLogic;
+import org.apache.syncope.core.logic.wa.MfaTrusStorageLogic;
 import org.apache.syncope.core.logic.wa.U2FRegistrationLogic;
 import org.apache.syncope.core.logic.wa.WAClientAppLogic;
 import org.apache.syncope.core.logic.wa.WAConfigLogic;
@@ -189,6 +190,16 @@ public class AMLogicContext {
         return new U2FRegistrationLogic(entityFactory, authProfileDAO, 
authProfileDataBinder);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public MfaTrusStorageLogic mfaTrusStorageLogic(
+            final AuthProfileDAO authProfileDAO,
+            final AuthProfileDataBinder authProfileDataBinder,
+            final EntityFactory entityFactory) {
+
+        return new MfaTrusStorageLogic(entityFactory, authProfileDAO, 
authProfileDataBinder);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public WAClientAppLogic waClientAppLogic(
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java
similarity index 66%
copy from 
core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
copy to 
core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java
index c12d81b8ba..eb1a110c27 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java
@@ -19,15 +19,17 @@
 package org.apache.syncope.core.logic.wa;
 
 import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.builder.CompareToBuilder;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
-import org.apache.syncope.common.lib.wa.U2FDevice;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
 import org.apache.syncope.core.logic.AbstractAuthProfileLogic;
 import org.apache.syncope.core.persistence.api.dao.AuthProfileDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
@@ -36,11 +38,11 @@ import 
org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
 import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
 import org.springframework.security.access.prepost.PreAuthorize;
 
-public class U2FRegistrationLogic extends AbstractAuthProfileLogic {
+public class MfaTrusStorageLogic extends AbstractAuthProfileLogic {
 
     protected final EntityFactory entityFactory;
 
-    public U2FRegistrationLogic(
+    public MfaTrusStorageLogic(
             final EntityFactory entityFactory,
             final AuthProfileDAO authProfileDAO,
             final AuthProfileDataBinder binder) {
@@ -50,60 +52,29 @@ public class U2FRegistrationLogic extends 
AbstractAuthProfileLogic {
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
-    public void create(final String owner, final U2FDevice device) {
-        AuthProfile profile = authProfileDAO.findByOwner(owner).orElseGet(() 
-> {
-            AuthProfile authProfile = 
entityFactory.newEntity(AuthProfile.class);
-            authProfile.setOwner(owner);
-            return authProfile;
-        });
-
-        List<U2FDevice> devices = profile.getU2FRegisteredDevices();
-        devices.add(device);
-        profile.setU2FRegisteredDevices(devices);
-        authProfileDAO.save(profile);
-    }
-
-    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
-    public void delete(final Long id, final OffsetDateTime expirationDate) {
-        List<AuthProfile> profiles = authProfileDAO.findAll(-1, -1);
-        profiles.forEach(profile -> {
-            List<U2FDevice> devices = profile.getU2FRegisteredDevices();
-            if (devices != null) {
-                if (id != null) {
-                    devices.removeIf(device -> device.getId() == id);
-                } else if (expirationDate != null) {
-                    devices.removeIf(device -> 
device.getIssueDate().compareTo(expirationDate) < 0);
-                } else {
-                    devices = List.of();
-                }
-                profile.setU2FRegisteredDevices(devices);
-                authProfileDAO.save(profile);
-            }
-        });
-    }
-
-    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
-    public Pair<Integer, List<U2FDevice>> search(
+    public Pair<Integer, List<MfaTrustedDevice>> search(
             final Integer page,
-            final Integer itemsPerPage, final Long id,
-            final OffsetDateTime expirationDate,
+            final Integer itemsPerPage,
+            final String principal,
+            final Long id,
+            final OffsetDateTime recordDate,
             final List<OrderByClause> orderByClauses) {
 
-        List<Comparator<U2FDevice>> comparatorList = orderByClauses.
+        List<Comparator<MfaTrustedDevice>> comparatorList = orderByClauses.
                 stream().
                 map(orderByClause -> {
-                    Comparator<U2FDevice> comparator = null;
+                    Comparator<MfaTrustedDevice> comparator = null;
                     if (orderByClause.getField().equals("id")) {
                         comparator = (o1, o2) -> new CompareToBuilder().
                                 append(o1.getId(), o2.getId()).toComparison();
                     }
-                    if (orderByClause.getField().equals("issueDate")) {
+                    if (orderByClause.getField().equals("expirationDate")) {
                         comparator = (o1, o2) -> new CompareToBuilder().
-                                append(o1.getIssueDate(), 
o2.getIssueDate()).toComparison();
+                                append(o1.getExpirationDate(), 
o2.getExpirationDate()).toComparison();
                     }
-                    if (orderByClause.getField().equals("record")) {
+                    if (orderByClause.getField().equals("recordDate")) {
                         comparator = (o1, o2) -> new CompareToBuilder().
-                                append(o1.getRecord(), 
o2.getRecord()).toComparison();
+                                append(o1.getRecordDate(), 
o2.getRecordDate()).toComparison();
                     }
                     if (comparator != null) {
                         if (orderByClause.getDirection() == 
OrderByClause.Direction.DESC) {
@@ -116,30 +87,32 @@ public class U2FRegistrationLogic extends 
AbstractAuthProfileLogic {
                 filter(Objects::nonNull).
                 collect(Collectors.toList());
 
-        List<U2FDevice> devices = authProfileDAO.findAll(-1, -1).
-                stream().
-                map(AuthProfile::getU2FRegisteredDevices).
-                filter(Objects::nonNull).
-                flatMap(List::stream).
+        List<MfaTrustedDevice> devices = (principal == null
+                ? authProfileDAO.findAll(-1, -1).stream().
+                        
map(AuthProfile::getMfaTrustedDevices).filter(Objects::nonNull).flatMap(List::stream)
+                : authProfileDAO.findByOwner(principal).
+                        
map(AuthProfile::getMfaTrustedDevices).filter(Objects::nonNull).map(List::stream).
+                        orElse(Stream.empty())).
                 filter(device -> {
                     EqualsBuilder builder = new EqualsBuilder();
+                    
builder.appendSuper(device.getExpirationDate().isAfter(ZonedDateTime.now()));
                     if (id != null) {
                         builder.append(id, (Long) device.getId());
                     }
-                    if (expirationDate != null) {
-                        
builder.appendSuper(device.getIssueDate().compareTo(expirationDate) >= 0);
+                    if (recordDate != null) {
+                        
builder.appendSuper(device.getRecordDate().isAfter(recordDate.toZonedDateTime()));
                     }
-                    return true;
+                    return builder.build();
                 }).
                 filter(Objects::nonNull).
                 collect(Collectors.toList());
 
-        List<U2FDevice> result = devices.stream().
+        List<MfaTrustedDevice> result = devices.stream().
                 limit(itemsPerPage).
                 skip(itemsPerPage * (page <= 0 ? 0L : page.longValue() - 1L)).
                 sorted((o1, o2) -> {
                     int compare;
-                    for (Comparator<U2FDevice> comparator : comparatorList) {
+                    for (Comparator<MfaTrustedDevice> comparator : 
comparatorList) {
                         compare = comparator.compare(o1, o2);
                         if (compare != 0) {
                             return compare;
@@ -150,4 +123,37 @@ public class U2FRegistrationLogic extends 
AbstractAuthProfileLogic {
                 .collect(Collectors.toList());
         return Pair.of(devices.size(), result);
     }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void create(final String owner, final MfaTrustedDevice device) {
+        AuthProfile profile = authProfileDAO.findByOwner(owner).orElseGet(() 
-> {
+            AuthProfile authProfile = 
entityFactory.newEntity(AuthProfile.class);
+            authProfile.setOwner(owner);
+            return authProfile;
+        });
+
+        List<MfaTrustedDevice> devices = profile.getMfaTrustedDevices();
+        devices.add(device);
+        profile.setMfaTrustedDevices(devices);
+        authProfileDAO.save(profile);
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void delete(final OffsetDateTime expirationDate, final String 
recordKey) {
+        List<AuthProfile> profiles = authProfileDAO.findAll(-1, -1);
+        profiles.forEach(profile -> {
+            List<MfaTrustedDevice> devices = profile.getMfaTrustedDevices();
+            if (devices != null) {
+                if (recordKey != null) {
+                    devices.removeIf(device -> 
recordKey.equals(device.getRecordKey()));
+                } else if (expirationDate != null) {
+                    devices.removeIf(device -> 
device.getExpirationDate().isBefore(expirationDate.toZonedDateTime()));
+                } else {
+                    devices = List.of();
+                }
+                profile.setMfaTrustedDevices(devices);
+                authProfileDAO.save(profile);
+            }
+        });
+    }
 }
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
index c12d81b8ba..31f7828832 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
@@ -85,7 +85,8 @@ public class U2FRegistrationLogic extends 
AbstractAuthProfileLogic {
     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
     public Pair<Integer, List<U2FDevice>> search(
             final Integer page,
-            final Integer itemsPerPage, final Long id,
+            final Integer itemsPerPage,
+            final Long id,
             final OffsetDateTime expirationDate,
             final List<OrderByClause> orderByClauses) {
 
@@ -129,7 +130,7 @@ public class U2FRegistrationLogic extends 
AbstractAuthProfileLogic {
                     if (expirationDate != null) {
                         
builder.appendSuper(device.getIssueDate().compareTo(expirationDate) >= 0);
                     }
-                    return true;
+                    return builder.build();
                 }).
                 filter(Objects::nonNull).
                 collect(Collectors.toList());
diff --git 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
index 06fd5faadf..81942ea9ec 100644
--- 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
@@ -29,6 +29,7 @@ import 
org.apache.syncope.common.rest.api.service.SRARouteService;
 import 
org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthAccountService;
 import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
 import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
 import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
 import org.apache.syncope.common.rest.api.service.wa.WAClientAppService;
 import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
@@ -44,6 +45,7 @@ import org.apache.syncope.core.logic.SRARouteLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic;
 import org.apache.syncope.core.logic.wa.ImpersonationLogic;
+import org.apache.syncope.core.logic.wa.MfaTrusStorageLogic;
 import org.apache.syncope.core.logic.wa.U2FRegistrationLogic;
 import org.apache.syncope.core.logic.wa.WAClientAppLogic;
 import org.apache.syncope.core.logic.wa.WAConfigLogic;
@@ -59,6 +61,7 @@ import 
org.apache.syncope.core.rest.cxf.service.SRARouteServiceImpl;
 import 
org.apache.syncope.core.rest.cxf.service.wa.GoogleMfaAuthAccountServiceImpl;
 import 
org.apache.syncope.core.rest.cxf.service.wa.GoogleMfaAuthTokenServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.wa.ImpersonationServiceImpl;
+import org.apache.syncope.core.rest.cxf.service.wa.MfaTrustStorageServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.wa.U2FRegistrationServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.wa.WAClientAppServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.wa.WAConfigServiceImpl;
@@ -146,6 +149,12 @@ public class AMRESTCXFContext {
         return new U2FRegistrationServiceImpl(u2fRegistrationLogic);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public MfaTrustStorageService mfaTrustStorageService(final 
MfaTrusStorageLogic mfaTrusStorageLogic) {
+        return new MfaTrustStorageServiceImpl(mfaTrusStorageLogic);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public WAClientAppService waClientAppService(final WAClientAppLogic 
waClientAppLogic) {
diff --git 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/MfaTrustStorageServiceImpl.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/MfaTrustStorageServiceImpl.java
new file mode 100644
index 0000000000..92e732d1aa
--- /dev/null
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/MfaTrustStorageServiceImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.syncope.core.rest.cxf.service.wa;
+
+import java.util.List;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
+import org.apache.syncope.core.logic.wa.MfaTrusStorageLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MfaTrustStorageServiceImpl extends AbstractService implements 
MfaTrustStorageService {
+
+    protected final MfaTrusStorageLogic logic;
+
+    public MfaTrustStorageServiceImpl(final MfaTrusStorageLogic logic) {
+        this.logic = logic;
+    }
+
+    @Override
+    public PagedResult<MfaTrustedDevice> search(final MfaTrustedDeviceQuery 
query) {
+        Pair<Integer, List<MfaTrustedDevice>> result = logic.search(
+                query.getPage(),
+                query.getSize(),
+                query.getPrincipal(),
+                query.getId(),
+                query.getRecordDate(),
+                getOrderByClauses(query.getOrderBy()));
+        return buildPagedResult(result.getRight(), query.getPage(), 
query.getSize(), result.getLeft());
+    }
+
+    @Override
+    public void create(final String owner, final MfaTrustedDevice device) {
+        logic.create(owner, device);
+    }
+
+    @Override
+    public void delete(final MfaTrustedDeviceQuery query) {
+        logic.delete(query.getExpirationDate(), query.getRecordKey());
+    }
+}
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
index 8abfefc15d..2bf4554de6 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
@@ -22,6 +22,7 @@ import java.util.List;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
 import org.apache.syncope.common.lib.wa.U2FDevice;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.syncope.core.persistence.api.entity.Entity;
@@ -44,6 +45,10 @@ public interface AuthProfile extends Entity {
 
     void setGoogleMfaAuthAccounts(List<GoogleMfaAuthAccount> accounts);
 
+    List<MfaTrustedDevice> getMfaTrustedDevices();
+
+    void setMfaTrustedDevices(List<MfaTrustedDevice> records);
+
     List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials();
 
     void setWebAuthnDeviceCredentials(List<WebAuthnDeviceCredential> 
credentials);
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
index 98761e7559..5e15eab2c2 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
@@ -30,6 +30,7 @@ import java.util.Optional;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
 import org.apache.syncope.common.lib.wa.U2FDevice;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
@@ -56,6 +57,10 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     protected static final TypeReference<List<U2FDevice>> U2F_TYPEREF = new 
TypeReference<List<U2FDevice>>() {
     };
 
+    protected static final TypeReference<List<MfaTrustedDevice>> 
MFA_TRUSTED_DEVICE_TYPEREF =
+            new TypeReference<List<MfaTrustedDevice>>() {
+    };
+
     protected static final TypeReference<List<ImpersonationAccount>> 
IMPERSONATION_TYPEREF =
             new TypeReference<List<ImpersonationAccount>>() {
     };
@@ -79,6 +84,9 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     @Lob
     private String u2fRegisteredDevices;
 
+    @Lob
+    private String mfaTrustedDevices;
+
     @Lob
     private String webAuthnDeviceCredentials;
 
@@ -121,8 +129,19 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     }
 
     @Override
-    public void setU2FRegisteredDevices(final List<U2FDevice> records) {
-        u2fRegisteredDevices = POJOHelper.serialize(records);
+    public void setU2FRegisteredDevices(final List<U2FDevice> devices) {
+        u2fRegisteredDevices = POJOHelper.serialize(devices);
+    }
+
+    @Override
+    public List<MfaTrustedDevice> getMfaTrustedDevices() {
+        return Optional.ofNullable(mfaTrustedDevices).
+                map(v -> POJOHelper.deserialize(v, 
MFA_TRUSTED_DEVICE_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
+    }
+
+    @Override
+    public void setMfaTrustedDevices(final List<MfaTrustedDevice> devices) {
+        mfaTrustedDevices = POJOHelper.serialize(devices);
     }
 
     @Override
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
index 79722e4ebe..584a020931 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
@@ -40,6 +40,7 @@ public class AuthProfileDataBinderImpl implements 
AuthProfileDataBinder {
         
authProfileTO.getGoogleMfaAuthTokens().addAll(authProfile.getGoogleMfaAuthTokens());
         
authProfileTO.getGoogleMfaAuthAccounts().addAll(authProfile.getGoogleMfaAuthAccounts());
         
authProfileTO.getU2FRegisteredDevices().addAll(authProfile.getU2FRegisteredDevices());
+        
authProfileTO.getMfaTrustedDevices().addAll(authProfile.getMfaTrustedDevices());
         
authProfileTO.getWebAuthnDeviceCredentials().addAll(authProfile.getWebAuthnDeviceCredentials());
         return authProfileTO;
     }
@@ -57,6 +58,7 @@ public class AuthProfileDataBinderImpl implements 
AuthProfileDataBinder {
         
authProfile.setGoogleMfaAuthTokens(authProfileTO.getGoogleMfaAuthTokens());
         
authProfile.setGoogleMfaAuthAccounts(authProfileTO.getGoogleMfaAuthAccounts());
         
authProfile.setU2FRegisteredDevices(authProfileTO.getU2FRegisteredDevices());
+        authProfile.setMfaTrustedDevices(authProfileTO.getMfaTrustedDevices());
         
authProfile.setWebAuthnDeviceCredentials(authProfileTO.getWebAuthnDeviceCredentials());
         return authProfile;
     }
diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
index 00a76404d1..257d225501 100644
--- 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
@@ -50,7 +50,7 @@ public class SCIMConfGeneralPanel extends SCIMConfTabPanel {
 
                     @Override
                     public void setObject(final Date object) {
-                        
scimGeneralConf.setCreationDate(DateOps.convert(object));
+                        
scimGeneralConf.setCreationDate(DateOps.toOffsetDateTime(object));
                     }
                 }, 
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
         creationDatePanel.setEnabled(false);
@@ -67,7 +67,7 @@ public class SCIMConfGeneralPanel extends SCIMConfTabPanel {
 
                     @Override
                     public void setObject(final Date object) {
-                        
scimGeneralConf.setLastChangeDate(DateOps.convert(object));
+                        
scimGeneralConf.setLastChangeDate(DateOps.toOffsetDateTime(object));
                     }
                 }, 
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
         lastChangeDatePanel.setEnabled(false);
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 2e54a6ee31..01eda292e0 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -159,6 +159,7 @@ import 
org.apache.syncope.common.rest.api.service.UserWorkflowTaskService;
 import 
org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthAccountService;
 import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
 import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
 import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
 import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
 import 
org.apache.syncope.common.rest.api.service.wa.WebAuthnRegistrationService;
@@ -385,6 +386,8 @@ public abstract class AbstractITCase {
 
     protected static U2FRegistrationService U2F_REGISTRATION_SERVICE;
 
+    protected static MfaTrustStorageService MFA_TRUST_STORAGE_SERVICE;
+
     protected static WebAuthnRegistrationService WEBAUTHN_REGISTRATION_SERVICE;
 
     protected static ImpersonationService IMPERSONATION_SERVICE;
@@ -418,6 +421,7 @@ public abstract class AbstractITCase {
         GOOGLE_MFA_AUTH_TOKEN_SERVICE = 
ANONYMOUS_CLIENT.getService(GoogleMfaAuthTokenService.class);
         GOOGLE_MFA_AUTH_ACCOUNT_SERVICE = 
ANONYMOUS_CLIENT.getService(GoogleMfaAuthAccountService.class);
         U2F_REGISTRATION_SERVICE = 
ANONYMOUS_CLIENT.getService(U2FRegistrationService.class);
+        MFA_TRUST_STORAGE_SERVICE = 
ANONYMOUS_CLIENT.getService(MfaTrustStorageService.class);
         WEBAUTHN_REGISTRATION_SERVICE = 
ANONYMOUS_CLIENT.getService(WebAuthnRegistrationService.class);
         IMPERSONATION_SERVICE = 
ANONYMOUS_CLIENT.getService(ImpersonationService.class);
 
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/MfaTrustStorageTCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/MfaTrustStorageTCase.java
new file mode 100644
index 0000000000..1a1e488ce9
--- /dev/null
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/MfaTrustStorageTCase.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.syncope.fit.core.wa;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.UUID;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class MfaTrustStorageTCase extends AbstractITCase {
+
+    private static MfaTrustedDevice createDeviceRegistration() {
+        MfaTrustedDevice device = new MfaTrustedDevice();
+        device.setId(System.currentTimeMillis());
+        device.setDeviceFingerprint(UUID.randomUUID().toString());
+        device.setName(UUID.randomUUID().toString());
+        device.setRecordKey(UUID.randomUUID().toString());
+        device.setRecordDate(ZonedDateTime.now());
+        device.setExpirationDate(ZonedDateTime.now().plusDays(30));
+        return device;
+    }
+
+    @BeforeEach
+    public void setup() {
+        MFA_TRUST_STORAGE_SERVICE.delete(new 
MfaTrustedDeviceQuery.Builder().build());
+    }
+
+    @Test
+    public void create() {
+        assertDoesNotThrow(() -> MFA_TRUST_STORAGE_SERVICE.create(
+                UUID.randomUUID().toString(), createDeviceRegistration()));
+    }
+
+    @Test
+    public void count() {
+        String owner = UUID.randomUUID().toString();
+        MfaTrustedDevice device = createDeviceRegistration();
+        MFA_TRUST_STORAGE_SERVICE.create(owner, device);
+
+        List<MfaTrustedDevice> devices = MFA_TRUST_STORAGE_SERVICE.search(new 
MfaTrustedDeviceQuery.Builder().
+                principal(owner).build()).getResult();
+        assertEquals(1, devices.size());
+
+        MFA_TRUST_STORAGE_SERVICE.delete(new 
MfaTrustedDeviceQuery.Builder().recordKey(device.getRecordKey()).build());
+
+        devices = MFA_TRUST_STORAGE_SERVICE.search(new 
MfaTrustedDeviceQuery.Builder().build()).getResult();
+        assertTrue(devices.isEmpty());
+    }
+
+    @Test
+    public void delete() {
+        MfaTrustedDevice device = createDeviceRegistration();
+        String owner = UUID.randomUUID().toString();
+        MFA_TRUST_STORAGE_SERVICE.create(owner, device);
+
+        MFA_TRUST_STORAGE_SERVICE.delete(new 
MfaTrustedDeviceQuery.Builder().recordKey(device.getRecordKey()).build());
+        assertTrue(MFA_TRUST_STORAGE_SERVICE.search(
+                new 
MfaTrustedDeviceQuery.Builder().id(device.getId()).build()).getResult().isEmpty());
+
+        OffsetDateTime date = OffsetDateTime.now().plusDays(1);
+
+        MFA_TRUST_STORAGE_SERVICE.delete(new 
MfaTrustedDeviceQuery.Builder().expirationDate(date).build());
+
+        assertTrue(MFA_TRUST_STORAGE_SERVICE.search(
+                new 
MfaTrustedDeviceQuery.Builder().id(device.getId()).build()).getResult().isEmpty());
+    }
+}
diff --git a/pom.xml b/pom.xml
index 554752dcc8..393ffb28a8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1356,7 +1356,7 @@ under the License.
         <plugin>
           <groupId>io.github.git-commit-id</groupId>
           <artifactId>git-commit-id-maven-plugin</artifactId>
-          <version>5.0.0</version>
+          <version>6.0.0</version>
           <executions>
             <execution>
               <goals>
@@ -1607,9 +1607,9 @@ under the License.
         <plugin>
           <groupId>us.hebi.sass</groupId>
           <artifactId>sass-cli-maven-plugin</artifactId>
-          <version>1.0.2</version>
+          <version>1.0.3</version>
           <configuration>
-            <sassVersion>1.57.1</sassVersion>
+            <sassVersion>1.62.0</sassVersion>
             <skip>${sass.skip}</skip>
           </configuration>
         </plugin>
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index c86776e873..ec9753feb2 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -329,6 +329,14 @@ under the License.
       <groupId>org.apereo.cas</groupId>
       <artifactId>cas-server-support-otp-mfa-core</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-trusted-mfa</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-trusted-mfa-core</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apereo.cas</groupId>
       <artifactId>cas-server-support-oidc-services</artifactId>
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
index 122597d506..9451b94c4d 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
@@ -26,6 +26,7 @@ import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.info.Contact;
 import io.swagger.v3.oas.models.info.Info;
 import io.swagger.v3.oas.models.security.SecurityScheme;
+import java.io.Serializable;
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.List;
@@ -57,6 +58,7 @@ import 
org.apache.syncope.wa.starter.mapping.RemoteEndpointAccessMapper;
 import org.apache.syncope.wa.starter.mapping.SAML2SPClientAppTOMapper;
 import org.apache.syncope.wa.starter.mapping.TicketExpirationMapper;
 import org.apache.syncope.wa.starter.mapping.TimeBasedAccessMapper;
+import 
org.apache.syncope.wa.starter.mfa.WAMultifactorAuthenticationTrustStorage;
 import org.apache.syncope.wa.starter.oidc.WAOIDCJWKSGeneratorService;
 import org.apache.syncope.wa.starter.pac4j.saml.WASAML2ClientCustomizer;
 import org.apache.syncope.wa.starter.saml.idp.WASamlIdPCasEventListener;
@@ -88,6 +90,8 @@ import 
org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerat
 import 
org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
 import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
 import 
org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
+import 
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecordKeyGenerator;
+import 
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustStorage;
 import org.apereo.cas.util.DateTimeUtils;
 import org.apereo.cas.util.LdapUtils;
 import org.apereo.cas.util.crypto.CipherExecutor;
@@ -325,6 +329,23 @@ public class WAContext {
         return new WAGoogleMfaAuthCredentialRepository(waRestClient, 
googleAuthenticatorInstance);
     }
 
+    @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
+    @Bean(name = MultifactorAuthenticationTrustStorage.BEAN_NAME)
+    public MultifactorAuthenticationTrustStorage mfaTrustStorage(
+            final CasConfigurationProperties casProperties,
+            @Qualifier("mfaTrustRecordKeyGenerator")
+            final MultifactorAuthenticationTrustRecordKeyGenerator 
keyGenerationStrategy,
+            @Qualifier("mfaTrustCipherExecutor")
+            final CipherExecutor<Serializable, String> mfaTrustCipherExecutor,
+            final WARestClient waRestClient) {
+
+        return new WAMultifactorAuthenticationTrustStorage(
+                casProperties.getAuthn().getMfa().getTrusted(),
+                mfaTrustCipherExecutor,
+                keyGenerationStrategy,
+                waRestClient);
+    }
+
     @Bean
     public OidcJsonWebKeystoreGeneratorService 
oidcJsonWebKeystoreGeneratorService(
             final CasConfigurationProperties casProperties,
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/mfa/WAMultifactorAuthenticationTrustStorage.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/mfa/WAMultifactorAuthenticationTrustStorage.java
new file mode 100644
index 0000000000..084b6e084b
--- /dev/null
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/mfa/WAMultifactorAuthenticationTrustStorage.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.syncope.wa.starter.mfa;
+
+import java.io.Serializable;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.apache.syncope.wa.starter.services.WAServiceRegistry;
+import 
org.apereo.cas.configuration.model.support.mfa.trusteddevice.TrustedDevicesMultifactorProperties;
+import 
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecord;
+import 
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecordKeyGenerator;
+import 
org.apereo.cas.trusted.authentication.storage.BaseMultifactorAuthenticationTrustStorage;
+import org.apereo.cas.util.crypto.CipherExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WAMultifactorAuthenticationTrustStorage extends 
BaseMultifactorAuthenticationTrustStorage {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(WAServiceRegistry.class);
+
+    protected static final int PAGE_SIZE = 500;
+
+    protected final WARestClient waRestClient;
+
+    public WAMultifactorAuthenticationTrustStorage(
+            final TrustedDevicesMultifactorProperties 
trustedDevicesMultifactorProperties,
+            final CipherExecutor<Serializable, String> cipherExecutor,
+            final MultifactorAuthenticationTrustRecordKeyGenerator 
keyGenerationStrategy,
+            final WARestClient waRestClient) {
+
+        super(trustedDevicesMultifactorProperties, cipherExecutor, 
keyGenerationStrategy);
+        this.waRestClient = waRestClient;
+    }
+
+    @Override
+    protected MultifactorAuthenticationTrustRecord saveInternal(final 
MultifactorAuthenticationTrustRecord record) {
+        MfaTrustedDevice device = new MfaTrustedDevice();
+        device.setRecordKey(record.getRecordKey());
+        device.setId(record.getId());
+        device.setName(record.getName());
+        device.setDeviceFingerprint(record.getDeviceFingerprint());
+        Optional.ofNullable(record.getExpirationDate()).
+                ifPresent(date -> 
device.setExpirationDate(date.toInstant().atZone(ZoneId.systemDefault())));
+        device.setRecordDate(record.getRecordDate());
+
+        LOG.trace("Saving multifactor authentication trust record [{}]", 
device);
+
+        
waRestClient.getService(MfaTrustStorageService.class).create(record.getPrincipal(),
 device);
+
+        return record;
+    }
+
+    @Override
+    public void remove(final ZonedDateTime expirationDate) {
+        waRestClient.getService(MfaTrustStorageService.class).delete(
+                new 
MfaTrustedDeviceQuery.Builder().expirationDate(expirationDate.toOffsetDateTime()).build());
+    }
+
+    @Override
+    public void remove(final String recordKey) {
+        waRestClient.getService(MfaTrustStorageService.class).delete(
+                new 
MfaTrustedDeviceQuery.Builder().recordKey(recordKey).build());
+    }
+
+    protected MultifactorAuthenticationTrustRecord translate(final 
MfaTrustedDevice device) {
+        MultifactorAuthenticationTrustRecord record = new 
MultifactorAuthenticationTrustRecord();
+        record.setRecordKey(device.getRecordKey());
+        record.setId(device.getId());
+        record.setName(device.getName());
+        record.setDeviceFingerprint(device.getDeviceFingerprint());
+        Optional.ofNullable(device.getExpirationDate()).
+                ifPresent(date -> 
record.setExpirationDate(Date.from(date.toInstant())));
+        record.setRecordDate(device.getRecordDate());
+        return record;
+    }
+
+    @Override
+    public Set<? extends MultifactorAuthenticationTrustRecord> getAll() {
+        if (!waRestClient.isReady()) {
+            LOG.debug("Syncope client is not yet ready to fetch MFA trusted 
device records");
+            return Set.of();
+        }
+
+        int count = waRestClient.getService(MfaTrustStorageService.class).
+                search(new 
MfaTrustedDeviceQuery.Builder().page(1).size(0).build()).getTotalCount();
+
+        Set<MultifactorAuthenticationTrustRecord> result = new HashSet<>();
+
+        for (int page = 1; page <= (count / PAGE_SIZE) + 1; page++) {
+            waRestClient.getService(MfaTrustStorageService.class).
+                    search(new 
MfaTrustedDeviceQuery.Builder().page(page).size(PAGE_SIZE).
+                            orderBy("expirationDate").build()).
+                    getResult().stream().
+                    map(this::translate).
+                    forEach(result::add);
+        }
+
+        return result;
+    }
+
+    @Override
+    public Set<? extends MultifactorAuthenticationTrustRecord> get(final 
ZonedDateTime onOrAfterDate) {
+        if (!waRestClient.isReady()) {
+            LOG.debug("Syncope client is not yet ready to fetch MFA trusted 
device records");
+            return Set.of();
+        }
+
+        return waRestClient.getService(MfaTrustStorageService.class).
+                search(new 
MfaTrustedDeviceQuery.Builder().recordDate(onOrAfterDate.toOffsetDateTime()).build()).
+                getResult().stream().
+                map(this::translate).
+                collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<? extends MultifactorAuthenticationTrustRecord> get(final 
String principal) {
+        if (!waRestClient.isReady()) {
+            LOG.debug("Syncope client is not yet ready to fetch MFA trusted 
device records");
+            return Set.of();
+        }
+
+        return waRestClient.getService(MfaTrustStorageService.class).
+                search(new 
MfaTrustedDeviceQuery.Builder().principal(principal).build()).getResult().stream().
+                map(this::translate).
+                collect(Collectors.toSet());
+    }
+
+    @Override
+    public MultifactorAuthenticationTrustRecord get(final long id) {
+        if (!waRestClient.isReady()) {
+            LOG.debug("Syncope client is not yet ready to fetch MFA trusted 
device records");
+            return null;
+        }
+
+        return waRestClient.getService(MfaTrustStorageService.class).
+                search(new 
MfaTrustedDeviceQuery.Builder().id(id).build()).getResult().stream().findFirst().
+                map(this::translate).
+                orElse(null);
+    }
+}
diff --git 
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/WAGoogleMfaAuthTokenRepositoryTest.java
 
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthTokenRepositoryTest.java
similarity index 96%
rename from 
wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/WAGoogleMfaAuthTokenRepositoryTest.java
rename to 
wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthTokenRepositoryTest.java
index b3089233da..e21bda8114 100644
--- 
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/WAGoogleMfaAuthTokenRepositoryTest.java
+++ 
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthTokenRepositoryTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.wa.starter.gauth.token;
+package org.apache.syncope.wa.starter.gauth;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 

Reply via email to