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;