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

mmoayyed 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 a8c39ef  SYNCOPE-1570: Support U2F device registration via REST APIs 
(#197)
a8c39ef is described below

commit a8c39efa167e9cc34a8d1d2c315454ae305a3a84
Author: Misagh Moayyed <[email protected]>
AuthorDate: Fri Jul 10 17:40:32 2020 +0430

    SYNCOPE-1570: Support U2F device registration via REST APIs (#197)
    
    Co-authored-by: Francesco Chicchiriccò <[email protected]>
---
 .../common/lib/auth/SAML2IdPAuthModuleConf.java    |   6 +-
 .../syncope/common/lib/types/AMEntitlement.java    |  10 +
 .../common/lib/types/U2FRegisteredDevice.java      | 163 ++++++++++++++
 .../common/rest/api/service/wa/U2FDeviceQuery.java | 110 ++++++++++
 .../api/service/wa/U2FRegistrationService.java     |  88 ++++++++
 .../core/logic/GoogleMfaAuthTokenLogic.java        |   7 +-
 .../syncope/core/logic/U2FRegistrationLogic.java   | 238 +++++++++++++++++++++
 .../cxf/service/wa/U2FRegistrationServiceImpl.java |  80 +++++++
 .../persistence/api/entity/auth/AuthProfile.java   |   9 +-
 .../jpa/entity/auth/JPAAuthProfile.java            |  25 +++
 .../persistence/jpa/inner/AuthProfileTest.java     |  36 +++-
 .../org/apache/syncope/fit/AbstractITCase.java     |   4 +
 .../syncope/fit/core/U2FRegistrationITCase.java    | 138 ++++++++++++
 pom.xml                                            |   7 +-
 .../bootstrap/SyncopeWAPropertySourceLocator.java  |   4 +-
 wa/starter/pom.xml                                 |   6 +-
 .../wa/starter/config/SyncopeWAConfiguration.java  |  44 ++--
 .../starter/u2f/SyncopeWAU2FDeviceRepository.java  | 167 +++++++++++++++
 .../wa/starter/SyncopeWAServiceRegistryTest.java   |   1 -
 19 files changed, 1117 insertions(+), 26 deletions(-)

diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
index 43bf7c3..c588d53 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
@@ -159,7 +159,7 @@ public class SAML2IdPAuthModuleConf extends 
AbstractAuthModuleConf {
     /**
      * Collection of signing signature blacklisted algorithms, if any, to 
override the global defaults.
      */
-    private final List<String> blackListedSignatureSigningAlgorithms = new 
ArrayList<>(0);
+    private final List<String> blockedSignatureSigningAlgorithms = new 
ArrayList<>(0);
 
     /**
      * Collection of signing signature algorithms, if any, to override the 
global defaults.
@@ -355,8 +355,8 @@ public class SAML2IdPAuthModuleConf extends 
AbstractAuthModuleConf {
         this.signServiceProviderLogoutRequest = 
signServiceProviderLogoutRequest;
     }
 
-    public List<String> getBlackListedSignatureSigningAlgorithms() {
-        return blackListedSignatureSigningAlgorithms;
+    public List<String> getBlockedSignatureSigningAlgorithms() {
+        return blockedSignatureSigningAlgorithms;
     }
 
     public List<String> getSignatureAlgorithms() {
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
index ca78af3..20b08e1 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
@@ -106,6 +106,16 @@ public final class AMEntitlement {
 
     public static final String OIDC_JWKS_DELETE = "OIDC_JWKS_DELETE";
 
+    public static final String U2F_DELETE_DEVICE = "U2F_DELETE_DEVICE";
+
+    public static final String U2F_SAVE_DEVICE = "U2F_SAVE_DEVICE";
+
+    public static final String U2F_READ_DEVICE = "U2F_READ_DEVICE";
+
+    public static final String U2F_SEARCH = "U2F_SEARCH";
+
+    public static final String U2F_UPDATE_DEVICE = "U2F_UPDATE_DEVICE";
+
     private static final Set<String> VALUES;
 
     static {
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/U2FRegisteredDevice.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/U2FRegisteredDevice.java
new file mode 100644
index 0000000..74450e1
--- /dev/null
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/U2FRegisteredDevice.java
@@ -0,0 +1,163 @@
+/*
+ * 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.types;
+
+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;
+
+import java.util.Date;
+import java.util.Optional;
+
+public class U2FRegisteredDevice implements BaseBean {
+
+    private static final long serialVersionUID = 1185073386484048953L;
+
+    private long id;
+
+    private String key;
+
+    private String record;
+
+    private String owner;
+
+    private Date issueDate;
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public String getRecord() {
+        return record;
+    }
+
+    public void setRecord(final String record) {
+        this.record = record;
+    }
+
+    public String getOwner() {
+        return owner;
+    }
+
+    public void setOwner(final String owner) {
+        this.owner = owner;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(final long id) {
+        this.id = id;
+    }
+
+    public Date getIssueDate() {
+        return Optional.ofNullable(this.issueDate).
+            map(date -> new Date(date.getTime())).orElse(null);
+    }
+
+    public void setIssueDate(final Date issueDate) {
+        this.issueDate = Optional.ofNullable(issueDate).
+            map(date -> new Date(date.getTime())).orElse(null);
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+            .appendSuper(super.hashCode())
+            .append(key)
+            .append(record)
+            .append(owner)
+            .append(id)
+            .append(issueDate)
+            .toHashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        U2FRegisteredDevice rhs = (U2FRegisteredDevice) obj;
+        return new EqualsBuilder()
+            .appendSuper(super.equals(obj))
+            .append(this.key, rhs.key)
+            .append(this.record, rhs.record)
+            .append(this.owner, rhs.owner)
+            .append(this.id, rhs.id)
+            .append(this.issueDate, rhs.issueDate)
+            .isEquals();
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+            .append("key", key)
+            .append("record", record)
+            .append("owner", owner)
+            .append("id", id)
+            .append("issueDate", issueDate)
+            .toString();
+    }
+
+    public static class Builder {
+
+        private final U2FRegisteredDevice instance = new U2FRegisteredDevice();
+
+        public U2FRegisteredDevice.Builder issueDate(final Date issued) {
+            instance.setIssueDate(issued);
+            return this;
+        }
+
+        public U2FRegisteredDevice.Builder record(final String record) {
+            instance.setRecord(record);
+            return this;
+        }
+
+        public U2FRegisteredDevice.Builder owner(final String owner) {
+            instance.setOwner(owner);
+            return this;
+        }
+
+        public U2FRegisteredDevice.Builder key(final String key) {
+            instance.setKey(key);
+            return this;
+        }
+
+        public U2FRegisteredDevice.Builder id(final long id) {
+            instance.setId(id);
+            return this;
+        }
+
+        public U2FRegisteredDevice build() {
+            return instance;
+        }
+    }
+}
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/U2FDeviceQuery.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/U2FDeviceQuery.java
new file mode 100644
index 0000000..a87ca51
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/U2FDeviceQuery.java
@@ -0,0 +1,110 @@
+/*
+ * 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.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.syncope.common.rest.api.beans.AbstractQuery;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+import javax.ws.rs.QueryParam;
+
+import java.util.Date;
+
+public class U2FDeviceQuery extends AbstractQuery {
+    private static final long serialVersionUID = -7381828286332101171L;
+
+    private Long id;
+
+    private String entityKey;
+
+    private Date expirationDate;
+
+    private String owner;
+
+    @Parameter(name = JAXRSService.PARAM_ENTITY_KEY, in = ParameterIn.QUERY,
+        schema = @Schema(implementation = String.class, example = 
"50592942-73ec-44c4-a377-e859524245e4"))
+    public String getEntityKey() {
+        return entityKey;
+    }
+
+    @QueryParam(JAXRSService.PARAM_ENTITY_KEY)
+    public void setEntityKey(final String entityKey) {
+        this.entityKey = entityKey;
+    }
+
+    @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 = "expirationDate", in = ParameterIn.QUERY, schema = 
@Schema(implementation = Date.class))
+    public Date getExpirationDate() {
+        return expirationDate;
+    }
+
+    @QueryParam("expirationDate")
+    public void setExpirationDate(final Date expirationDate) {
+        this.expirationDate = expirationDate;
+    }
+
+    @Parameter(name = "owner", in = ParameterIn.QUERY, schema = 
@Schema(implementation = String.class))
+    public String getOwner() {
+        return owner;
+    }
+
+    @QueryParam("owner")
+    public void setOwner(final String owner) {
+        this.owner = owner;
+    }
+
+    public static class Builder extends AbstractQuery.Builder<U2FDeviceQuery, 
U2FDeviceQuery.Builder> {
+        @Override
+        protected U2FDeviceQuery newInstance() {
+            return new U2FDeviceQuery();
+        }
+
+        public U2FDeviceQuery.Builder entityKey(final String entityKey) {
+            getInstance().setEntityKey(entityKey);
+            return this;
+        }
+
+        public U2FDeviceQuery.Builder owner(final String owner) {
+            getInstance().setOwner(owner);
+            return this;
+        }
+
+        public U2FDeviceQuery.Builder id(final Long id) {
+            getInstance().setId(id);
+            return this;
+        }
+
+        public U2FDeviceQuery.Builder expirationDate(final Date date) {
+            getInstance().setExpirationDate(date);
+            return this;
+        }
+    }
+}
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/U2FRegistrationService.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/U2FRegistrationService.java
new file mode 100644
index 0000000..4a47c25
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/U2FRegistrationService.java
@@ -0,0 +1,88 @@
+/*
+ * 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.headers.Header;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+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 org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Tag(name = "U2F Registrations")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer")})
+@Path("wa/u2f")
+public interface U2FRegistrationService extends JAXRSService {
+    @DELETE
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Path("devices")
+    Response delete(@BeanParam U2FDeviceQuery query);
+
+    @ApiResponses({
+        @ApiResponse(responseCode = "201",
+            description = "U2FRegistration successfully created", headers = {
+            @Header(name = RESTHeaders.RESOURCE_KEY, schema =
+            @Schema(type = "string"),
+                description = "UUID generated for the entity created")})})
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Path("devices")
+    Response create(@NotNull U2FRegisteredDevice acct);
+
+    @PUT
+    @Path("devices")
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    void update(@NotNull U2FRegisteredDevice acct);
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Path("devices")
+    PagedResult<U2FRegisteredDevice> search(@BeanParam U2FDeviceQuery query);
+
+    @GET
+    @Path("{key}")
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    U2FRegisteredDevice read(@NotNull @PathParam("key") String key);
+}
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
index 759b1d0..94bfcbc 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
@@ -115,7 +115,6 @@ public class GoogleMfaAuthTokenLogic extends 
AbstractTransactionalLogic<AuthProf
                 filter(t -> t.getToken().equals(token.getToken())).
                 findFirst().
                 orElse(null);
-
     }
 
     @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_READ_TOKEN + "') "
@@ -191,9 +190,9 @@ public class GoogleMfaAuthTokenLogic extends 
AbstractTransactionalLogic<AuthProf
                 return authProfileDAO.findByKey(key).
                         map(authProfileDataBinder::getAuthProfileTO).
                         orElseThrow();
-            } catch (Throwable ignore) {
-                LOG.debug("Unresolved reference", ignore);
-                throw new UnresolvedReferenceException(ignore);
+            } catch (final Throwable ex) {
+                LOG.debug("Unresolved reference", ex);
+                throw new UnresolvedReferenceException(ex);
             }
         }
 
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/U2FRegistrationLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/U2FRegistrationLogic.java
new file mode 100644
index 0000000..a6232cc
--- /dev/null
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/U2FRegistrationLogic.java
@@ -0,0 +1,238 @@
+/*
+ * 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.logic;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+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.to.AuthProfileTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
+import org.apache.syncope.core.persistence.api.dao.auth.AuthProfileDAO;
+import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.lang.reflect.Method;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Component
+public class U2FRegistrationLogic extends 
AbstractTransactionalLogic<AuthProfileTO> {
+    @Autowired
+    private AuthProfileDAO authProfileDAO;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Autowired
+    private AuthProfileDataBinder authProfileDataBinder;
+
+    @Override
+    protected AuthProfileTO resolveReference(final Method method, final 
Object... args)
+        throws UnresolvedReferenceException {
+        String key = null;
+        if (ArrayUtils.isNotEmpty(args)) {
+            for (int i = 0; key == null && i < args.length; i++) {
+                if (args[i] instanceof String) {
+                    key = (String) args[i];
+                } else if (args[i] instanceof AuthProfileTO) {
+                    key = ((AuthProfileTO) args[i]).getKey();
+                }
+            }
+        }
+
+        if (key != null) {
+            try {
+                return authProfileDAO.findByKey(key).
+                    map(authProfileDataBinder::getAuthProfileTO).
+                    orElseThrow();
+            } catch (final Throwable e) {
+                LOG.debug("Unresolved reference", e);
+                throw new UnresolvedReferenceException(e);
+            }
+        }
+        throw new UnresolvedReferenceException();
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.U2F_SAVE_DEVICE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public U2FRegisteredDevice save(final U2FRegisteredDevice acct) {
+        AuthProfile profile = authProfileDAO.findByOwner(acct.getOwner()).
+            orElseGet(() -> {
+                final AuthProfile authProfile = 
entityFactory.newEntity(AuthProfile.class);
+                authProfile.setOwner(acct.getOwner());
+                return authProfile;
+            });
+
+        if (acct.getKey() == null) {
+            acct.setKey(SecureRandomUtils.generateRandomUUID().toString());
+        }
+        profile.add(acct);
+        profile = authProfileDAO.save(profile);
+        return profile.getU2FRegisteredDevices().
+            stream().
+            filter(Objects::nonNull).
+            filter(t -> t.getKey().equals(acct.getKey())).
+            findFirst().
+            orElse(null);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.U2F_READ_DEVICE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public U2FRegisteredDevice read(final String key) {
+        return authProfileDAO.findAll().
+            stream().
+            map(AuthProfile::getU2FRegisteredDevices).
+            filter(Objects::nonNull).
+            flatMap(List::stream).
+            filter(record -> record.getKey().equals(key)).
+            findFirst().
+            orElse(null);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.U2F_DELETE_DEVICE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void delete(final String entityKey, final Long id, final Date 
expirationDate) {
+        List<AuthProfile> profiles = authProfileDAO.findAll();
+        profiles.forEach(profile -> {
+            List<U2FRegisteredDevice> devices = 
profile.getU2FRegisteredDevices();
+            if (devices != null) {
+                if (StringUtils.isNotBlank(entityKey)) {
+                    devices.removeIf(device -> 
device.getKey().equals(entityKey));
+                } else 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('" + AMEntitlement.U2F_SEARCH + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public Pair<Integer, List<U2FRegisteredDevice>> search(final String 
entityKey, final Integer page,
+                                                           final Integer 
itemsPerPage, final Long id,
+                                                           final Date 
expirationDate,
+                                                           final 
List<OrderByClause> orderByClauses) {
+        List<Comparator<U2FRegisteredDevice>> comparatorList = orderByClauses.
+            stream().
+            map(orderByClause -> {
+                Comparator<U2FRegisteredDevice> comparator = null;
+                if (orderByClause.getField().equals("id")) {
+                    comparator = (o1, o2) -> new 
CompareToBuilder().append(o1.getId(), o2.getId()).toComparison();
+                }
+                if (orderByClause.getField().equals("owner")) {
+                    comparator = (o1, o2) -> new 
CompareToBuilder().append(o1.getOwner(), o2.getOwner()).toComparison();
+                }
+                if (orderByClause.getField().equals("key")) {
+                    comparator = (o1, o2) -> new 
CompareToBuilder().append(o1.getKey(), o2.getKey()).toComparison();
+                }
+                if (orderByClause.getField().equals("issueDate")) {
+                    comparator = (o1, o2) ->
+                        new CompareToBuilder().append(o1.getIssueDate(), 
o2.getIssueDate()).toComparison();
+                }
+                if (orderByClause.getField().equals("record")) {
+                    comparator = (o1, o2) ->
+                        new CompareToBuilder().append(o1.getRecord(), 
o2.getRecord()).toComparison();
+                }
+                if (comparator != null) {
+                    if (orderByClause.getDirection() == 
OrderByClause.Direction.DESC) {
+                        return comparator.reversed();
+                    }
+                    return comparator;
+                }
+                return null;
+            }).
+            filter(Objects::nonNull).
+            collect(Collectors.toList());
+
+        List<U2FRegisteredDevice> devices = authProfileDAO.findAll().
+            stream().
+            map(AuthProfile::getU2FRegisteredDevices).
+            filter(Objects::nonNull).
+            flatMap(List::stream).
+            filter(device -> {
+                EqualsBuilder builder = new EqualsBuilder();
+                if (StringUtils.isNotBlank(entityKey)) {
+                    builder.append(entityKey, device.getKey());
+                }
+                if (id != null) {
+                    builder.append(id, (Long) device.getId());
+                }
+                if (expirationDate != null) {
+                    
builder.appendSuper(device.getIssueDate().compareTo(expirationDate) >= 0);
+                }
+                return true;
+            }).
+            filter(Objects::nonNull).
+            collect(Collectors.toList());
+
+        List<U2FRegisteredDevice> pagedResults = devices.
+            stream().
+            limit(itemsPerPage).
+            skip(itemsPerPage * (page <= 0 ? 0 : page - 1)).
+            sorted((o1, o2) -> {
+                int result;
+                for (Comparator<U2FRegisteredDevice> comparator : 
comparatorList) {
+                    result = comparator.compare(o1, o2);
+                    if (result != 0) {
+                        return result;
+                    }
+                }
+                return 0;
+            })
+            .collect(Collectors.toList());
+        return Pair.of(devices.size(), pagedResults);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.U2F_UPDATE_DEVICE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void update(final U2FRegisteredDevice acct) {
+        List<AuthProfile> profiles = authProfileDAO.findAll();
+        profiles.forEach(profile -> {
+            List<U2FRegisteredDevice> devices = 
profile.getU2FRegisteredDevices();
+            if (devices != null) {
+                if (devices.removeIf(device -> 
device.getKey().equals(acct.getKey()))) {
+                    devices.add(acct);
+                    profile.setU2FRegisteredDevices(devices);
+                    authProfileDAO.save(profile);
+                }
+            }
+        });
+    }
+}
diff --git 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/U2FRegistrationServiceImpl.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/U2FRegistrationServiceImpl.java
new file mode 100644
index 0000000..16912be
--- /dev/null
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/U2FRegistrationServiceImpl.java
@@ -0,0 +1,80 @@
+/*
+ * 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 org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.wa.U2FDeviceQuery;
+import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
+import org.apache.syncope.core.logic.U2FRegistrationLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+import java.util.List;
+
+@Service
+public class U2FRegistrationServiceImpl extends AbstractServiceImpl implements 
U2FRegistrationService {
+    @Autowired
+    private U2FRegistrationLogic logic;
+
+    @Override
+    public Response delete(final U2FDeviceQuery query) {
+        logic.delete(query.getEntityKey(), query.getId(), 
query.getExpirationDate());
+        return Response.noContent().build();
+    }
+
+    @Override
+    public void update(final U2FRegisteredDevice acct) {
+        logic.update(acct);
+    }
+
+    @Override
+    public Response create(final U2FRegisteredDevice acct) {
+        final U2FRegisteredDevice token = logic.save(acct);
+        URI location = 
uriInfo.getAbsolutePathBuilder().path(token.getKey()).build();
+        return Response.created(location).
+            header(RESTHeaders.RESOURCE_KEY, token.getKey()).
+            entity(token).
+            build();
+    }
+
+    @Override
+    public PagedResult<U2FRegisteredDevice> search(final U2FDeviceQuery query) 
{
+        Pair<Integer, List<U2FRegisteredDevice>> result = logic.search(
+            query.getEntityKey(),
+            query.getPage(),
+            query.getSize(),
+            query.getId(),
+            query.getExpirationDate(),
+            getOrderByClauses(query.getOrderBy()));
+        return buildPagedResult(result.getRight(), query.getPage(), 
query.getSize(), result.getLeft());
+    }
+    
+    @Override
+    public U2FRegisteredDevice read(final String key) {
+        return logic.read(key);
+    }
+}
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
index 4834388..b6428b7 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.persistence.api.entity.auth;
 
 import org.apache.syncope.common.lib.types.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
 import org.apache.syncope.core.persistence.api.entity.Entity;
 
 import java.util.List;
@@ -30,11 +31,15 @@ public interface AuthProfile extends Entity {
     String getOwner();
 
     void setOwner(String owner);
-    
+
     List<GoogleMfaAuthToken> getGoogleMfaAuthTokens();
 
     void setGoogleMfaAuthTokens(List<GoogleMfaAuthToken> tokens);
 
+    List<U2FRegisteredDevice> getU2FRegisteredDevices();
+
+    void setU2FRegisteredDevices(List<U2FRegisteredDevice> records);
+
     List<GoogleMfaAuthAccount> getGoogleMfaAuthAccounts();
 
     void setGoogleMfaAuthAccounts(List<GoogleMfaAuthAccount> accounts);
@@ -42,4 +47,6 @@ public interface AuthProfile extends Entity {
     void add(GoogleMfaAuthToken token);
 
     void add(GoogleMfaAuthAccount account);
+
+    void add(U2FRegisteredDevice account);
 }
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
index f5e8089..a830b1b 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.persistence.jpa.entity.auth;
 import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.syncope.common.lib.types.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
 import 
org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
@@ -43,6 +44,9 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     private static final long serialVersionUID = 57352617217394093L;
 
     @Lob
+    private String u2fRegisteredDevices;
+
+    @Lob
     private String googleMfaAuthAccounts;
 
     @Lob
@@ -96,10 +100,31 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     }
 
     @Override
+    public List<U2FRegisteredDevice> getU2FRegisteredDevices() {
+        return u2fRegisteredDevices == null
+            ? new ArrayList<>(0)
+            : POJOHelper.deserialize(u2fRegisteredDevices, new 
TypeReference<List<U2FRegisteredDevice>>() {
+        });
+    }
+
+    @Override
+    public void setU2FRegisteredDevices(final List<U2FRegisteredDevice> 
records) {
+        this.u2fRegisteredDevices = POJOHelper.serialize(records);
+    }
+
+    @Override
     public void add(final GoogleMfaAuthAccount account) {
         checkType(account, GoogleMfaAuthAccount.class);
         final List<GoogleMfaAuthAccount> accounts = getGoogleMfaAuthAccounts();
         accounts.add(account);
         setGoogleMfaAuthAccounts(accounts);
     }
+
+    @Override
+    public void add(final U2FRegisteredDevice registration) {
+        checkType(registration, U2FRegisteredDevice.class);
+        final List<U2FRegisteredDevice> records = getU2FRegisteredDevices();
+        records.add(registration);
+        setU2FRegisteredDevices(records);
+    }
 }
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
index 070cc1e..137e799 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.persistence.jpa.inner;
 
 import org.apache.syncope.common.lib.types.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
 import org.apache.syncope.core.persistence.api.dao.auth.AuthProfileDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
@@ -69,7 +70,28 @@ public class AuthProfileTest extends AbstractTest {
         assertTrue(result.isPresent());
 
         authProfile.setOwner("SyncopeCreate-New");
-        authProfile.getGoogleMfaAuthTokens().clear();
+        authProfile.setGoogleMfaAuthTokens(List.of());
+        authProfileDAO.save(authProfile);
+
+        assertFalse(authProfileDAO.findByOwner(id).isPresent());
+    }
+
+    @Test
+    public void u2fRegisteredDevice() {
+        String id = SecureRandomUtils.generateRandomUUID().toString();
+        createAuthProfileWithU2FDevice(id, "{ 'record': 1 }");
+
+        Optional<AuthProfile> result = authProfileDAO.findByOwner(id);
+        assertTrue(result.isPresent());
+
+        assertFalse(authProfileDAO.findAll().isEmpty());
+
+        AuthProfile authProfile = result.get();
+        result = authProfileDAO.findByKey(authProfile.getKey());
+        assertTrue(result.isPresent());
+
+        authProfile.setOwner("SyncopeCreate-NewU2F");
+        authProfile.setU2FRegisteredDevices(List.of());
         authProfileDAO.save(authProfile);
 
         assertFalse(authProfileDAO.findByOwner(id).isPresent());
@@ -112,6 +134,18 @@ public class AuthProfileTest extends AbstractTest {
         return authProfileDAO.save(profile);
     }
 
+    private AuthProfile createAuthProfileWithU2FDevice(final String owner, 
final String record) {
+        AuthProfile profile = entityFactory.newEntity(AuthProfile.class);
+        profile.setOwner(owner);
+        U2FRegisteredDevice token = new U2FRegisteredDevice.Builder()
+            .issueDate(new Date())
+            .record(record)
+            .owner(owner)
+            .build();
+        profile.add(token);
+        return authProfileDAO.save(profile);
+    }
+
     private AuthProfile createAuthProfileWithAccount(final String owner) {
         AuthProfile profile = entityFactory.newEntity(AuthProfile.class);
         profile.setOwner(owner);
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 4e57459..881063f 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
@@ -149,6 +149,7 @@ import 
org.apache.syncope.common.rest.api.service.BpmnProcessService;
 import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataConfService;
 import org.apache.syncope.common.rest.api.service.wa.SAML2IdPMetadataService;
 import org.apache.syncope.common.rest.api.service.UserWorkflowTaskService;
+import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
 import org.apache.syncope.fit.core.CoreITContext;
 import org.apache.syncope.fit.core.UserITCase;
 import org.identityconnectors.common.security.Encryptor;
@@ -349,6 +350,8 @@ public abstract class AbstractITCase {
 
     protected static OIDCJWKSConfService oidcJwksConfService;
 
+    protected static U2FRegistrationService u2FRegistrationService;
+
     @BeforeAll
     public static void securitySetup() {
         try (InputStream propStream = 
Encryptor.class.getResourceAsStream("/security.properties")) {
@@ -431,6 +434,7 @@ public abstract class AbstractITCase {
         authProfileService = adminClient.getService(AuthProfileService.class);
         oidcJwksService = adminClient.getService(OIDCJWKSService.class);
         oidcJwksConfService = 
adminClient.getService(OIDCJWKSConfService.class);
+        u2FRegistrationService = 
adminClient.getService(U2FRegistrationService.class);
     }
 
     @Autowired
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/U2FRegistrationITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/U2FRegistrationITCase.java
new file mode 100644
index 0000000..4d75b8f
--- /dev/null
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/U2FRegistrationITCase.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.wa.U2FDeviceQuery;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.core.Response;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class U2FRegistrationITCase extends AbstractITCase {
+    private static U2FRegisteredDevice createDeviceRegistration() {
+        return new U2FRegisteredDevice.Builder()
+            .owner(UUID.randomUUID().toString())
+            .issueDate(new Date())
+            .id(System.currentTimeMillis())
+            
.record("{\"keyHandle\":\"2_QYgDSPYcOgYBGBe8c9PVCunjigbD-3o5HcliXhu-Up_GKckYMxxVF6AgSPWubqfWy8WmJNDYQEJ1QKZe343Q\","
 +
+                
"\"publicKey\":\"BMj46cH-lHkRMovZhrusmm_fYL_sFausDPJIDZfx4pIiRqRNtasd4vU3yJyrTXXbdxyD36GZLx1WKLHGmApv7Nk\""
 +
+                ",\"counter\":-1,\"compromised\":false}")
+            .build();
+    }
+
+    @BeforeEach
+    public void setup() {
+        u2FRegistrationService.delete(new U2FDeviceQuery.Builder().build());
+    }
+
+    @Test
+    public void create() {
+        U2FRegisteredDevice acct = createDeviceRegistration();
+        assertDoesNotThrow(() -> {
+            Response response = u2FRegistrationService.create(acct);
+            if (response.getStatusInfo().getStatusCode() != 
Response.Status.CREATED.getStatusCode()) {
+                Exception ex = 
clientFactory.getExceptionMapper().fromResponse(response);
+                if (ex != null) {
+                    throw ex;
+                }
+            }
+        });
+    }
+
+    @Test
+    public void count() {
+        U2FRegisteredDevice acct = createDeviceRegistration();
+        Response response = u2FRegistrationService.create(acct);
+        String key = response.getHeaderString(RESTHeaders.RESOURCE_KEY);
+        assertNotNull(u2FRegistrationService.read(key));
+        Date date = 
Date.from(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+        U2FDeviceQuery query = new U2FDeviceQuery.Builder()
+            .owner(acct.getOwner())
+            .expirationDate(date)
+            .build();
+        List<U2FRegisteredDevice> devices = 
u2FRegistrationService.search(query).getResult();
+        assertEquals(1, devices.size());
+
+        query = new U2FDeviceQuery.Builder()
+            .id(acct.getId())
+            .build();
+        u2FRegistrationService.delete(query);
+
+        query = new U2FDeviceQuery.Builder().build();
+        devices = u2FRegistrationService.search(query).getResult();
+        assertTrue(devices.isEmpty());
+    }
+
+    @Test
+    public void delete() {
+        U2FRegisteredDevice acct1 = createDeviceRegistration();
+        Response response = u2FRegistrationService.create(acct1);
+        String key = response.getHeaderString(RESTHeaders.RESOURCE_KEY);
+        assertNotNull(u2FRegistrationService.read(key));
+
+        U2FDeviceQuery query = new U2FDeviceQuery.Builder()
+            .entityKey(key)
+            .build();
+        u2FRegistrationService.delete(query);
+        assertNull(u2FRegistrationService.read(key));
+
+        Date date = Date.from(LocalDate.now().plusDays(1)
+            .atStartOfDay(ZoneId.systemDefault()).toInstant());
+        query = new U2FDeviceQuery.Builder()
+            .expirationDate(date)
+            .build();
+        u2FRegistrationService.delete(query);
+
+        query = new U2FDeviceQuery.Builder()
+            .expirationDate(date)
+            .build();
+        assertTrue(u2FRegistrationService.search(query).getResult().isEmpty());
+    }
+
+    @Test
+    public void update() {
+        U2FRegisteredDevice acct1 = createDeviceRegistration();
+        Response response = u2FRegistrationService.create(acct1);
+        String key = response.getHeaderString(RESTHeaders.RESOURCE_KEY);
+        acct1 = u2FRegistrationService.read(key);
+        assertNotNull(acct1);
+        acct1.setOwner("NewOwner");
+        u2FRegistrationService.update(acct1);
+        acct1 = u2FRegistrationService.read(key);
+        assertEquals("NewOwner", acct1.getOwner());
+
+    }
+}
diff --git a/pom.xml b/pom.xml
index 2d54e20..133129c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1634,7 +1634,12 @@ under the License.
       </dependency>
       <dependency>
         <groupId>org.apereo.cas</groupId>
-        <artifactId>cas-server-support-rest-service-registry</artifactId>
+        <artifactId>cas-server-support-u2f</artifactId>
+        <version>${cas.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apereo.cas</groupId>
+        <artifactId>cas-server-support-u2f-core</artifactId>
         <version>${cas.version}</version>
       </dependency>
       <dependency>
diff --git 
a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
 
b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
index c707a92..ad9d115 100644
--- 
a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
+++ 
b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
@@ -27,8 +27,8 @@ import 
org.apereo.cas.configuration.model.support.generic.AcceptAuthenticationPr
 import 
org.apereo.cas.configuration.model.support.jaas.JaasAuthenticationProperties;
 import 
org.apereo.cas.configuration.model.support.jdbc.authn.QueryJdbcAuthenticationProperties;
 import 
org.apereo.cas.configuration.model.support.ldap.LdapAuthenticationProperties;
-import 
org.apereo.cas.configuration.model.support.mfa.u2f.U2FMultifactorProperties;
 import 
org.apereo.cas.configuration.model.support.mfa.gauth.GoogleAuthenticatorMultifactorProperties;
+import 
org.apereo.cas.configuration.model.support.mfa.u2f.U2FMultifactorProperties;
 import 
org.apereo.cas.configuration.model.support.pac4j.oidc.Pac4jGenericOidcClientProperties;
 import 
org.apereo.cas.configuration.model.support.pac4j.oidc.Pac4jOidcClientProperties;
 import 
org.apereo.cas.configuration.model.support.pac4j.saml.Pac4jSamlClientProperties;
@@ -263,7 +263,7 @@ public class SyncopeWAPropertySourceLocator implements 
PropertySourceLocator {
         
props.setAttributeConsumingServiceIndex(conf.getAttributeConsumingServiceIndex());
         props.setAuthnContextClassRef(conf.getAuthnContextClassRefs());
         
props.setAuthnContextComparisonType(conf.getAuthnContextComparisonType());
-        
props.setBlockedSignatureSigningAlgorithms(conf.getBlackListedSignatureSigningAlgorithms());
+        
props.setBlockedSignatureSigningAlgorithms(conf.getBlockedSignatureSigningAlgorithms());
         props.setDestinationBinding(conf.getDestinationBinding());
         
props.setIdentityProviderMetadataPath(conf.getIdentityProviderMetadataPath());
         props.setKeystoreAlias(conf.getKeystoreAlias());
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 30f2d15..5f57937 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -216,7 +216,11 @@ under the License.
     </dependency>
     <dependency>
       <groupId>org.apereo.cas</groupId>
-      <artifactId>cas-server-support-rest-service-registry</artifactId>
+      <artifactId>cas-server-support-u2f</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-u2f-core</artifactId>
     </dependency>
     <dependency>
       <groupId>org.apereo.cas</groupId>
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
index 423cdea..343497b 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
@@ -18,8 +18,10 @@
  */
 package org.apache.syncope.wa.starter.config;
 
+import org.apereo.cas.adaptors.u2f.storage.U2FDeviceRepository;
 import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
 import org.apereo.cas.configuration.CasConfigurationProperties;
+import 
org.apereo.cas.configuration.model.support.mfa.u2f.U2FMultifactorProperties;
 import org.apereo.cas.oidc.jwks.OidcJsonWebKeystoreGeneratorService;
 import 
org.apereo.cas.otp.repository.credentials.OneTimeTokenCredentialRepository;
 import org.apereo.cas.otp.repository.token.OneTimeTokenRepository;
@@ -30,9 +32,13 @@ 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.idp.metadata.writer.SamlIdPCertificateAndKeyWriter;
+import org.apereo.cas.util.DateTimeUtils;
 import org.apereo.cas.util.crypto.CipherExecutor;
 
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
 import com.warrenstrange.googleauth.IGoogleAuthenticator;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStart;
 import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStop;
@@ -54,16 +60,19 @@ import 
org.apache.syncope.wa.starter.pac4j.saml.SyncopeWASAML2ClientCustomizer;
 import 
org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataGenerator;
 import 
org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataLocator;
 import org.apache.syncope.wa.starter.services.SyncopeWAServiceRegistry;
+import org.apache.syncope.wa.starter.u2f.SyncopeWAU2FDeviceRepository;
 import org.pac4j.core.client.Client;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.context.ApplicationContext;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import java.time.LocalDate;
+import java.time.ZoneId;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
@@ -85,39 +94,37 @@ public class SyncopeWAConfiguration {
     @Qualifier("serviceRegistryListeners")
     private Collection<ServiceRegistryListener> serviceRegistryListeners;
 
-    @Autowired
-    private ApplicationContext ctx;
-
     @ConditionalOnMissingBean
     @Bean
     public RegisteredServiceMapper registeredServiceMapper() {
         Map<String, AuthMapper> authPolicyConfMappers = new HashMap<>();
-        ctx.getBeansOfType(AuthMapper.class).forEach((name, bean) -> {
-            AuthMapFor authMapFor = ctx.findAnnotationOnBean(name, 
AuthMapFor.class);
+        applicationContext.getBeansOfType(AuthMapper.class).forEach((name, 
bean) -> {
+            AuthMapFor authMapFor = 
applicationContext.findAnnotationOnBean(name, AuthMapFor.class);
             if (authMapFor != null) {
                 
authPolicyConfMappers.put(authMapFor.authPolicyConfClass().getName(), bean);
             }
         });
 
         Map<String, AccessMapper> accessPolicyConfMappers = new HashMap<>();
-        ctx.getBeansOfType(AccessMapper.class).forEach((name, bean) -> {
-            AccessMapFor accessMapFor = ctx.findAnnotationOnBean(name, 
AccessMapFor.class);
+        applicationContext.getBeansOfType(AccessMapper.class).forEach((name, 
bean) -> {
+            AccessMapFor accessMapFor = 
applicationContext.findAnnotationOnBean(name, AccessMapFor.class);
             if (accessMapFor != null) {
                 
accessPolicyConfMappers.put(accessMapFor.accessPolicyConfClass().getName(), 
bean);
             }
         });
 
         Map<String, AttrReleaseMapper> attrReleasePolicyConfMappers = new 
HashMap<>();
-        ctx.getBeansOfType(AttrReleaseMapper.class).forEach((name, bean) -> {
-            AttrReleaseMapFor attrReleaseMapFor = 
ctx.findAnnotationOnBean(name, AttrReleaseMapFor.class);
+        
applicationContext.getBeansOfType(AttrReleaseMapper.class).forEach((name, bean) 
-> {
+            AttrReleaseMapFor attrReleaseMapFor =
+                applicationContext.findAnnotationOnBean(name, 
AttrReleaseMapFor.class);
             if (attrReleaseMapFor != null) {
                 
attrReleasePolicyConfMappers.put(attrReleaseMapFor.attrReleasePolicyConfClass().getName(),
 bean);
             }
         });
 
         Map<String, ClientAppMapper> clientAppTOMappers = new HashMap<>();
-        ctx.getBeansOfType(ClientAppMapper.class).forEach((name, bean) -> {
-            ClientAppMapFor clientAppMapFor = ctx.findAnnotationOnBean(name, 
ClientAppMapFor.class);
+        
applicationContext.getBeansOfType(ClientAppMapper.class).forEach((name, bean) 
-> {
+            ClientAppMapFor clientAppMapFor = 
applicationContext.findAnnotationOnBean(name, ClientAppMapFor.class);
             if (clientAppMapFor != null) {
                 
clientAppTOMappers.put(clientAppMapFor.clientAppClass().getName(), bean);
             }
@@ -193,6 +200,19 @@ public class SyncopeWAConfiguration {
     }
 
     @Bean
+    @Autowired
+    @RefreshScope
+    public U2FDeviceRepository u2fDeviceRepository(final WARestClient 
restClient) {
+        U2FMultifactorProperties u2f = 
casProperties.getAuthn().getMfa().getU2f();
+        final LocalDate expirationDate = LocalDate.now(ZoneId.systemDefault())
+            .minus(u2f.getExpireDevices(), 
DateTimeUtils.toChronoUnit(u2f.getExpireDevicesTimeUnit()));
+        final LoadingCache<String, String> requestStorage = 
Caffeine.newBuilder()
+            .expireAfterWrite(u2f.getExpireRegistrations(), 
u2f.getExpireRegistrationsTimeUnit())
+            .build(key -> StringUtils.EMPTY);
+        return new SyncopeWAU2FDeviceRepository(requestStorage, restClient, 
expirationDate);
+    }
+
+    @Bean
     public KeymasterStart keymasterStart() {
         return new KeymasterStart(NetworkService.Type.WA);
     }
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/u2f/SyncopeWAU2FDeviceRepository.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/u2f/SyncopeWAU2FDeviceRepository.java
new file mode 100644
index 0000000..2f18e60
--- /dev/null
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/u2f/SyncopeWAU2FDeviceRepository.java
@@ -0,0 +1,167 @@
+/*
+ * 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.u2f;
+
+import org.apereo.cas.adaptors.u2f.storage.BaseU2FDeviceRepository;
+import org.apereo.cas.adaptors.u2f.storage.U2FDeviceRegistration;
+import org.apereo.cas.util.crypto.CipherExecutor;
+
+import com.github.benmanes.caffeine.cache.LoadingCache;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.U2FRegisteredDevice;
+import org.apache.syncope.common.rest.api.service.wa.U2FDeviceQuery;
+import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class SyncopeWAU2FDeviceRepository extends BaseU2FDeviceRepository {
+    private static final Logger LOG = 
LoggerFactory.getLogger(SyncopeWAU2FDeviceRepository.class);
+
+    private final WARestClient waRestClient;
+
+    private final LocalDate expirationDate;
+
+    public SyncopeWAU2FDeviceRepository(final LoadingCache<String, String> 
requestStorage,
+                                        final WARestClient waRestClient,
+                                        final LocalDate expirationDate) {
+        super(requestStorage, CipherExecutor.noOpOfSerializableToString());
+        this.waRestClient = waRestClient;
+        this.expirationDate = expirationDate;
+    }
+
+    private static U2FDeviceRegistration parseRegistrationRecord(final 
U2FRegisteredDevice record) {
+        try {
+            return U2FDeviceRegistration.builder().
+                id(record.getId()).
+                username(record.getOwner()).
+                record(record.getRecord()).
+                createdDate(record.getIssueDate().
+                    toInstant().
+                    atZone(ZoneId.systemDefault()).
+                    toLocalDate()).
+                build();
+        } catch (final Exception e) {
+            LOG.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+    @Override
+    public Collection<? extends U2FDeviceRegistration> 
getRegisteredDevices(final String owner) {
+        U2FDeviceQuery query = new U2FDeviceQuery.Builder()
+            .owner(owner)
+            .expirationDate(Date.from(Instant.from(expirationDate)))
+            .build();
+        final List<U2FRegisteredDevice> records = 
getU2FService().search(query).getResult();
+        return records.
+            stream().
+            map(SyncopeWAU2FDeviceRepository::parseRegistrationRecord).
+            filter(Objects::nonNull).
+            collect(Collectors.toList());
+    }
+
+    @Override
+    public Collection<? extends U2FDeviceRegistration> getRegisteredDevices() {
+        U2FDeviceQuery query = new U2FDeviceQuery.Builder()
+            .expirationDate(Date.from(Instant.from(expirationDate)))
+            .build();
+        final List<U2FRegisteredDevice> records = 
getU2FService().search(query).getResult();
+        return records.
+            stream().
+            map(SyncopeWAU2FDeviceRepository::parseRegistrationRecord).
+            filter(Objects::nonNull).
+            collect(Collectors.toList());
+    }
+
+    @Override
+    public U2FDeviceRegistration registerDevice(final U2FDeviceRegistration 
registration) {
+        U2FRegisteredDevice record = new U2FRegisteredDevice.Builder().
+            issueDate(Date.from(registration.getCreatedDate().atStartOfDay()
+                .atZone(ZoneId.systemDefault())
+                .toInstant())).
+            owner(registration.getUsername()).
+            record(registration.getRecord()).
+            id(registration.getId()).
+            build();
+        Response response = getU2FService().create(record);
+        return parseRegistrationRecord(response.readEntity(new 
GenericType<U2FRegisteredDevice>() {
+        }));
+    }
+
+    @Override
+    public void deleteRegisteredDevice(final U2FDeviceRegistration 
registration) {
+        U2FDeviceQuery query = new U2FDeviceQuery.Builder()
+            .id(registration.getId())
+            .build();
+        getU2FService().delete(query);
+    }
+
+    @Override
+    public boolean isDeviceRegisteredFor(final String owner) {
+        try {
+            Collection<? extends U2FDeviceRegistration> devices = 
getRegisteredDevices(owner);
+            return devices != null && !devices.isEmpty();
+        } catch (final SyncopeClientException e) {
+            if (e.getType() == ClientExceptionType.NotFound) {
+                LOG.info("Could not locate account for owner {}", owner);
+            } else {
+                LOG.error(e.getMessage(), e);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void clean() {
+        Date date = Date.from(expirationDate.atStartOfDay()
+            .atZone(ZoneId.systemDefault())
+            .toInstant());
+        U2FDeviceQuery query = new U2FDeviceQuery.Builder()
+            .expirationDate(date)
+            .build();
+        getU2FService().delete(query);
+    }
+
+    @Override
+    public void removeAll() {
+        getU2FService().delete(new U2FDeviceQuery.Builder().build());
+    }
+
+    private U2FRegistrationService getU2FService() {
+        if (!WARestClient.isReady()) {
+            throw new RuntimeException("Syncope core is not yet ready");
+        }
+        return 
waRestClient.getSyncopeClient().getService(U2FRegistrationService.class);
+    }
+}
diff --git 
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeWAServiceRegistryTest.java
 
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeWAServiceRegistryTest.java
index d336ac2..c69fcdb 100644
--- 
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeWAServiceRegistryTest.java
+++ 
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeWAServiceRegistryTest.java
@@ -21,7 +21,6 @@ package org.apache.syncope.wa.starter;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Collection;

Reply via email to