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 c016426 SYNCOPE-1562: Manage tokens for WA GoogleAuth MFA (#189)
c016426 is described below
commit c016426bc5f23b0f5f369866b3b7ac7449f1d6a9
Author: Misagh Moayyed <[email protected]>
AuthorDate: Mon Jun 1 17:51:20 2020 +0430
SYNCOPE-1562: Manage tokens for WA GoogleAuth MFA (#189)
---
.../syncope/common/lib/to/AuthProfileTO.java | 120 ++++++++++++
.../syncope/common/lib/types/AMEntitlement.java | 14 ++
.../common/lib/types/GoogleMfaAuthToken.java | 150 +++++++++++++++
.../rest/api/service/AuthProfileService.java | 77 ++++++++
.../api/service/wa/GoogleMfaAuthTokenService.java | 127 ++++++++++++
.../syncope/core/logic/AuthProfileLogic.java | 107 +++++++++++
.../core/logic/GoogleMfaAuthTokenLogic.java | 213 +++++++++++++++++++++
.../syncope/core/logic/SAML2IdPMetadataLogic.java | 2 +-
.../syncope/core/logic/SAML2SPKeystoreLogic.java | 2 +-
.../syncope/core/logic/SAML2SPMetadataLogic.java | 2 +-
.../rest/cxf/service/AuthProfileServiceImpl.java | 63 ++++++
.../service/wa/GoogleMfaAuthTokenServiceImpl.java | 104 ++++++++++
.../persistence/api/dao/auth/AuthProfileDAO.java | 43 +++++
.../persistence/api/entity/auth/AuthProfile.java | 38 ++++
.../jpa/dao/auth/JPAAuthProfileDAO.java | 91 +++++++++
.../persistence/jpa/entity/JPAEntityFactory.java | 4 +
.../jpa/entity/auth/JPAAuthProfile.java | 81 ++++++++
.../persistence/jpa/inner/AuthProfileTest.java | 79 ++++++++
.../api/data/AuthProfileDataBinder.java | 29 +++
.../java/data/AuthProfileDataBinderImpl.java | 50 +++++
.../org/apache/syncope/fit/AbstractITCase.java | 8 +
.../syncope/fit/core/GoogleMfaAuthTokenITCase.java | 147 ++++++++++++++
pom.xml | 15 ++
wa/starter/pom.xml | 12 ++
.../syncope/wa/starter/SyncopeWAConfiguration.java | 9 +
.../SyncopeWAGoogleMfaAuthTokenRepository.java | 143 ++++++++++++++
.../wa/starter/SyncopeCoreTestingServer.java | 94 ++++++++-
.../SyncopeWAGoogleMfaAuthTokenRepositoryTest.java | 43 +++++
28 files changed, 1860 insertions(+), 7 deletions(-)
diff --git
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
new file mode 100644
index 0000000..4c35578
--- /dev/null
+++
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
@@ -0,0 +1,120 @@
+/*
+ * 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.to;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+
+import javax.ws.rs.PathParam;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@XmlRootElement(name = "authProfile")
+@XmlType
+public class AuthProfileTO extends BaseBean implements EntityTO {
+
+ private static final long serialVersionUID = -6543425997956703057L;
+
+ private final List<GoogleMfaAuthToken> googleMfaAuthTokens = new
ArrayList<>();
+
+ private String key;
+
+ private String owner;
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @PathParam("key")
+ @Override
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(final String owner) {
+ this.owner = owner;
+ }
+
+ @XmlElementWrapper(name = "googleMfaAuthTokens")
+ @XmlElement(name = "googleMfaAuthTokens")
+ @JsonProperty("googleMfaAuthTokens")
+ public List<GoogleMfaAuthToken> getGoogleMfaAuthTokens() {
+ return googleMfaAuthTokens;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder().
+ append(key).
+ append(owner).
+ append(googleMfaAuthTokens).
+ build();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ AuthProfileTO other = (AuthProfileTO) obj;
+ return new EqualsBuilder().
+ append(key, other.key).
+ append(owner, other.owner).
+ append(googleMfaAuthTokens, other.googleMfaAuthTokens).
+ build();
+ }
+
+ public static class Builder {
+
+ private final AuthProfileTO instance = new AuthProfileTO();
+
+ public AuthProfileTO.Builder owner(final String owner) {
+ instance.setOwner(owner);
+ return this;
+ }
+
+ public AuthProfileTO.Builder key(final String key) {
+ instance.setKey(key);
+ return this;
+ }
+
+ public AuthProfileTO build() {
+ return instance;
+ }
+ }
+}
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 6f28b1f..73647c7 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
@@ -72,6 +72,20 @@ public final class AMEntitlement {
public static final String SAML2_SP_KEYSTORE_READ =
"SAML2_SP_KEYSTORE_READ";
+ public static final String GOOGLE_MFA_DELETE_TOKEN =
"GOOGLE_MFA_DELETE_TOKEN";
+
+ public static final String GOOGLE_MFA_SAVE_TOKEN = "GOOGLE_MFA_SAVE_TOKEN";
+
+ public static final String GOOGLE_MFA_READ_TOKEN = "GOOGLE_MFA_READ_TOKEN";
+
+ public static final String GOOGLE_MFA_COUNT_TOKEN =
"GOOGLE_MFA_COUNT_TOKEN";
+
+ public static final String AUTH_PROFILE_DELETE = "AUTH_PROFILE_DELETE";
+
+ public static final String AUTH_PROFILE_READ = "AUTH_PROFILE_READ";
+
+ public static final String AUTH_PROFILE_LIST = "AUTH_PROFILE_LIST";
+
private static final Set<String> VALUES;
static {
diff --git
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GoogleMfaAuthToken.java
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GoogleMfaAuthToken.java
new file mode 100644
index 0000000..5812756
--- /dev/null
+++
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GoogleMfaAuthToken.java
@@ -0,0 +1,150 @@
+/*
+ * 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 javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Optional;
+
+@XmlRootElement(name = "googleMfaAuthToken")
+@XmlType
+public class GoogleMfaAuthToken implements Serializable {
+ private static final long serialVersionUID = 2185073386484048953L;
+
+ private String key;
+
+ private Integer token;
+
+ private String owner;
+
+ private Date issueDate;
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ public Integer getToken() {
+ return token;
+ }
+
+ public void setToken(final Integer token) {
+ this.token = token;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(final String owner) {
+ this.owner = owner;
+ }
+
+ 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(token)
+ .append(owner)
+ .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;
+ }
+ GoogleMfaAuthToken rhs = (GoogleMfaAuthToken) obj;
+ return new EqualsBuilder()
+ .appendSuper(super.equals(obj))
+ .append(this.key, rhs.key)
+ .append(this.token, rhs.token)
+ .append(this.owner, rhs.owner)
+ .append(this.issueDate, rhs.issueDate)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("key", key)
+ .append("token", token)
+ .append("owner", owner)
+ .append("issueDate", issueDate)
+ .toString();
+ }
+
+ public static class Builder {
+
+ private final GoogleMfaAuthToken instance = new GoogleMfaAuthToken();
+
+ public GoogleMfaAuthToken.Builder issueDate(final Date issued) {
+ instance.setIssueDate(issued);
+ return this;
+ }
+
+ public GoogleMfaAuthToken.Builder token(final Integer token) {
+ instance.setToken(token);
+ return this;
+ }
+
+ public GoogleMfaAuthToken.Builder owner(final String owner) {
+ instance.setOwner(owner);
+ return this;
+ }
+
+ public GoogleMfaAuthToken.Builder key(final String key) {
+ instance.setKey(key);
+ return this;
+ }
+
+ public GoogleMfaAuthToken build() {
+ return instance;
+ }
+ }
+}
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
new file mode 100644
index 0000000..a58a3ad
--- /dev/null
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+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.AuthProfileTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+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;
+
+import java.util.List;
+
+/**
+ * REST operations for SAML 2.0 SP metadata.
+ */
+@Tag(name = "Auth Profiles")
+@SecurityRequirements({
+ @SecurityRequirement(name = "BasicAuthentication"),
+ @SecurityRequirement(name = "Bearer")})
+@Path("authProfiles")
+public interface AuthProfileService extends JAXRSService {
+
+ @GET
+ @Produces({MediaType.APPLICATION_JSON})
+ List<AuthProfileTO> list();
+
+ @GET
+ @Path("owners/{owner}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ AuthProfileTO findByOwner(@NotNull @PathParam("owner") String owner);
+
+ @GET
+ @Path("{key}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ AuthProfileTO findByKey(@NotNull @PathParam("key") String key);
+
+ @DELETE
+ @Path("{key}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ Response deleteByKey(@NotNull @PathParam("key") String key);
+
+ @DELETE
+ @Path("owners/{owner}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ Response deleteByOwner(@NotNull @PathParam("owner") String owner);
+
+}
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/GoogleMfaAuthTokenService.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/GoogleMfaAuthTokenService.java
new file mode 100644
index 0000000..9d8525b
--- /dev/null
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/GoogleMfaAuthTokenService.java
@@ -0,0 +1,127 @@
+/*
+ * 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.types.GoogleMfaAuthToken;
+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.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.util.Date;
+import java.util.List;
+
+@Tag(name = "Google MFA Tokens")
+@SecurityRequirements({
+ @SecurityRequirement(name = "BasicAuthentication"),
+ @SecurityRequirement(name = "Bearer")})
+@Path("wa/gauth")
+public interface GoogleMfaAuthTokenService extends JAXRSService {
+
+ @DELETE
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens")
+ Response deleteTokensByDate(@NotNull @QueryParam("expirationDate") Date
expirationDate);
+
+ @DELETE
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/${owner}/${token}")
+ Response deleteToken(@NotNull @PathParam("owner") String owner, @NotNull
@PathParam("token") Integer token);
+
+ @DELETE
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/owners/${owner}")
+ Response deleteTokensFor(@NotNull @PathParam("owner") String owner);
+
+ @DELETE
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/${token}")
+ Response deleteToken(@NotNull @PathParam("token") Integer token);
+
+ @DELETE
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens")
+ Response deleteTokens();
+
+ @ApiResponses({
+ @ApiResponse(responseCode = "201",
+ description = "GoogleMfaAuthToken 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("tokens")
+ Response save(@NotNull GoogleMfaAuthToken token);
+
+ @GET
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/${owner}/${token}")
+ GoogleMfaAuthToken findTokenFor(@NotNull @PathParam("owner") String owner,
+ @NotNull @PathParam("token") Integer
token);
+
+ @GET
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/owners/${owner}")
+ List<GoogleMfaAuthToken> findTokensFor(@NotNull @PathParam("owner") String
owner);
+
+ @GET
+ @Path("tokens/{key}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ GoogleMfaAuthToken findTokenFor(@NotNull @PathParam("key") String key);
+
+ @GET
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/${owner}/count")
+ long countTokensForOwner(@NotNull @PathParam("owner") String owner);
+
+ @GET
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Path("tokens/count")
+ long countTokens();
+}
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AuthProfileLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AuthProfileLogic.java
new file mode 100644
index 0000000..d66e6dc
--- /dev/null
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AuthProfileLogic.java
@@ -0,0 +1,107 @@
+/*
+ * 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.syncope.common.lib.to.AuthProfileTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.auth.AuthProfileDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+public class AuthProfileLogic extends
AbstractTransactionalLogic<AuthProfileTO> {
+ @Autowired
+ private AuthProfileDAO authProfileDAO;
+
+ @Autowired
+ private AuthProfileDataBinder authProfileDataBinder;
+
+ @PreAuthorize("hasRole('" + AMEntitlement.AUTH_PROFILE_DELETE + "') ")
+ public void deleteByKey(final String key) {
+ authProfileDAO.deleteByKey(key);
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.AUTH_PROFILE_DELETE + "') ")
+ public void deleteByOwner(final String owner) {
+ authProfileDAO.deleteByOwner(owner);
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.AUTH_PROFILE_READ + "') ")
+ public AuthProfileTO findByOwner(final String owner) {
+ AuthProfile authProfile =
authProfileDAO.findByOwner(owner).orElse(null);
+ if (authProfile == null) {
+ throw new NotFoundException(owner + " not found");
+ }
+ return authProfileDataBinder.getAuthProfileTO(authProfile);
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.AUTH_PROFILE_READ + "') ")
+ public AuthProfileTO findByKey(final String key) {
+ AuthProfile authProfile = authProfileDAO.findByKey(key).orElse(null);
+ if (authProfile == null) {
+ throw new NotFoundException(key + " not found");
+ }
+ return authProfileDataBinder.getAuthProfileTO(authProfile);
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.AUTH_PROFILE_LIST + "')")
+ public List<AuthProfileTO> list() {
+ return authProfileDAO.findAll().
+ stream().
+ map(authProfileDataBinder::getAuthProfileTO).
+ collect(Collectors.toList());
+ }
+
+ @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 findByKey(key);
+ } catch (Throwable ignore) {
+ LOG.debug("Unresolved reference", ignore);
+ throw new UnresolvedReferenceException(ignore);
+ }
+ }
+
+ throw new UnresolvedReferenceException();
+ }
+}
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
new file mode 100644
index 0000000..84b10b4
--- /dev/null
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
@@ -0,0 +1,213 @@
+/*
+ * 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.syncope.common.lib.to.AuthProfileTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+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;
+import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
+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.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+@Component
+public class GoogleMfaAuthTokenLogic extends
AbstractTransactionalLogic<AuthProfileTO> {
+ @Autowired
+ private AuthProfileDAO authProfileDAO;
+
+ @Autowired
+ private EntityFactory entityFactory;
+
+ @Autowired
+ private AuthProfileDataBinder authProfileDataBinder;
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_DELETE_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void delete(final Date expirationDate) {
+ authProfileDAO.
+ findAll().
+ forEach(profile -> removeTokenAndSave(profile,
+ token -> token.getIssueDate().compareTo(expirationDate) >= 0));
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_DELETE_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void delete(final String owner, final Integer otp) {
+ authProfileDAO.findByOwner(owner).
+ ifPresent(profile -> removeTokenAndSave(profile,
+ token -> token.getToken().equals(otp)));
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_DELETE_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void delete(final String owner) {
+ authProfileDAO.findByOwner(owner).ifPresent(profile -> {
+ profile.setGoogleMfaAuthTokens(List.of());
+ authProfileDAO.save(profile);
+ });
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_DELETE_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void delete(final Integer otp) {
+ authProfileDAO.findAll().
+ forEach(profile -> removeTokenAndSave(profile,
+ token -> token.getToken().equals(otp)));
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_DELETE_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void deleteAll() {
+ authProfileDAO.findAll().
+ forEach(profile -> {
+ profile.setGoogleMfaAuthTokens(List.of());
+ authProfileDAO.save(profile);
+ });
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_SAVE_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public GoogleMfaAuthToken save(final GoogleMfaAuthToken token) {
+ AuthProfile profile = authProfileDAO.findByOwner(token.getOwner()).
+ orElseGet(() -> {
+ final AuthProfile authProfile =
entityFactory.newEntity(AuthProfile.class);
+ authProfile.setOwner(token.getOwner());
+ return authProfile;
+ });
+
+ if (token.getKey() == null) {
+ token.setKey(UUID.randomUUID().toString());
+ }
+ profile.add(token);
+ profile = authProfileDAO.save(profile);
+ return profile.getGoogleMfaAuthTokens().
+ stream().
+ filter(t -> t.getToken().equals(token.getToken())).
+ findFirst().
+ orElse(null);
+
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_READ_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ @Transactional(readOnly = true)
+ public GoogleMfaAuthToken read(final String owner, final Integer otp) {
+ return authProfileDAO.findByOwner(owner).
+ stream().
+ map(AuthProfile::getGoogleMfaAuthTokens).
+ flatMap(List::stream).
+ filter(token -> token.getToken().equals(otp)).
+ findFirst().
+ orElseThrow(() -> new NotFoundException("Could not find token for
Owner " + owner + " and otp " + otp));
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_READ_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ @Transactional(readOnly = true)
+ public GoogleMfaAuthToken read(final String key) {
+ return authProfileDAO.findAll().
+ stream().
+ map(AuthProfile::getGoogleMfaAuthTokens).
+ flatMap(List::stream).
+ filter(token -> token.getKey().equals(key)).
+ findFirst().
+ orElse(null);
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_COUNT_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ @Transactional(readOnly = true)
+ public long countTokensFor(final String owner) {
+ return authProfileDAO.findByOwner(owner).
+ stream().
+ mapToLong(profile -> profile.getGoogleMfaAuthTokens().size()).
+ sum();
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_COUNT_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ @Transactional(readOnly = true)
+ public long countAll() {
+ return authProfileDAO.findAll().
+ stream().
+ mapToLong(profile -> profile.getGoogleMfaAuthTokens().size()).
+ sum();
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.GOOGLE_MFA_READ_TOKEN + "') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ @Transactional(readOnly = true)
+ public List<GoogleMfaAuthToken> findTokensFor(final String owner) {
+ return authProfileDAO.findByOwner(owner).
+ map(profile -> new ArrayList<>(profile.getGoogleMfaAuthTokens())).
+ orElse(new ArrayList<>(0));
+ }
+
+ private void removeTokenAndSave(final AuthProfile profile, final
Predicate<GoogleMfaAuthToken> criteria) {
+ List<GoogleMfaAuthToken> tokens = profile.getGoogleMfaAuthTokens();
+ boolean removed = tokens.removeIf(criteria);
+ if (removed) {
+ profile.setGoogleMfaAuthTokens(tokens);
+ authProfileDAO.save(profile);
+ }
+ }
+
+ @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 (Throwable ignore) {
+ LOG.debug("Unresolved reference", ignore);
+ throw new UnresolvedReferenceException(ignore);
+ }
+ }
+
+ throw new UnresolvedReferenceException();
+ }
+}
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
index a6f4930..87a0461 100644
---
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
@@ -101,7 +101,7 @@ public class SAML2IdPMetadataLogic extends
AbstractTransactionalLogic<SAML2IdPMe
if (args[i] instanceof String) {
appliesTo = (String) args[i];
} else if (args[i] instanceof SAML2IdPMetadataTO) {
- appliesTo = ((SAML2IdPMetadataTO) args[i]).getKey();
+ appliesTo = ((SAML2IdPMetadataTO) args[i]).getAppliesTo();
}
}
}
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java
index 103850c..48c9a36 100644
---
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java
@@ -95,7 +95,7 @@ public class SAML2SPKeystoreLogic extends
AbstractTransactionalLogic<SAML2SPKeys
if (args[i] instanceof String) {
name = (String) args[i];
} else if (args[i] instanceof SAML2SPKeystoreTO) {
- name = ((SAML2SPKeystoreTO) args[i]).getKey();
+ name = ((SAML2SPKeystoreTO) args[i]).getOwner();
}
}
}
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java
index 09182bc..5894ae4 100644
---
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java
@@ -95,7 +95,7 @@ public class SAML2SPMetadataLogic extends
AbstractTransactionalLogic<SAML2SPMeta
if (args[i] instanceof String) {
name = (String) args[i];
} else if (args[i] instanceof SAML2SPMetadataTO) {
- name = ((SAML2SPMetadataTO) args[i]).getKey();
+ name = ((SAML2SPMetadataTO) args[i]).getOwner();
}
}
}
diff --git
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuthProfileServiceImpl.java
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuthProfileServiceImpl.java
new file mode 100644
index 0000000..180d600
--- /dev/null
+++
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuthProfileServiceImpl.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import org.apache.syncope.common.lib.to.AuthProfileTO;
+import org.apache.syncope.common.rest.api.service.AuthProfileService;
+import org.apache.syncope.core.logic.AuthProfileLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.ws.rs.core.Response;
+
+import java.util.List;
+
+@Service
+public class AuthProfileServiceImpl extends AbstractServiceImpl implements
AuthProfileService {
+
+ @Autowired
+ private AuthProfileLogic logic;
+
+ @Override
+ public Response deleteByKey(final String key) {
+ logic.deleteByKey(key);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteByOwner(final String owner) {
+ logic.deleteByOwner(owner);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public AuthProfileTO findByOwner(final String owner) {
+ return logic.findByOwner(owner);
+ }
+
+ @Override
+ public AuthProfileTO findByKey(final String key) {
+ return logic.findByKey(key);
+ }
+
+ @Override
+ public List<AuthProfileTO> list() {
+ return logic.list();
+ }
+}
diff --git
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/GoogleMfaAuthTokenServiceImpl.java
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/GoogleMfaAuthTokenServiceImpl.java
new file mode 100644
index 0000000..96a0411
--- /dev/null
+++
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/GoogleMfaAuthTokenServiceImpl.java
@@ -0,0 +1,104 @@
+/*
+ * 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.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
+import org.apache.syncope.core.logic.GoogleMfaAuthTokenLogic;
+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.Date;
+import java.util.List;
+
+@Service
+public class GoogleMfaAuthTokenServiceImpl extends AbstractServiceImpl
implements GoogleMfaAuthTokenService {
+ @Autowired
+ private GoogleMfaAuthTokenLogic logic;
+
+ @Override
+ public Response deleteTokensByDate(final Date expirationDate) {
+ logic.delete(expirationDate);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteToken(final String owner, final Integer otp) {
+ logic.delete(owner, otp);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteTokensFor(final String owner) {
+ logic.delete(owner);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteToken(final Integer otp) {
+ logic.delete(otp);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteTokens() {
+ logic.deleteAll();
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response save(final GoogleMfaAuthToken tokenTO) {
+ final GoogleMfaAuthToken token = logic.save(tokenTO);
+ URI location =
uriInfo.getAbsolutePathBuilder().path(token.getKey()).build();
+ return Response.created(location).
+ header(RESTHeaders.RESOURCE_KEY, token.getKey()).
+ build();
+ }
+
+ @Override
+ public GoogleMfaAuthToken findTokenFor(final String owner, final Integer
otp) {
+ return logic.read(owner, otp);
+ }
+
+ @Override
+ public List<GoogleMfaAuthToken> findTokensFor(final String owner) {
+ return logic.findTokensFor(owner);
+ }
+
+ @Override
+ public GoogleMfaAuthToken findTokenFor(final String key) {
+ return logic.read(key);
+ }
+
+ @Override
+ public long countTokensForOwner(final String owner) {
+ return logic.countTokensFor(owner);
+ }
+
+ @Override
+ public long countTokens() {
+ return logic.countAll();
+ }
+}
diff --git
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/AuthProfileDAO.java
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/AuthProfileDAO.java
new file mode 100644
index 0000000..a464884
--- /dev/null
+++
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/AuthProfileDAO.java
@@ -0,0 +1,43 @@
+/*
+ * 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.persistence.api.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.DAO;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface AuthProfileDAO extends DAO<AuthProfile> {
+
+ List<AuthProfile> findAll();
+
+ Optional<AuthProfile> findByOwner(String owner);
+
+ Optional<AuthProfile> findByKey(String key);
+
+ AuthProfile save(AuthProfile profile);
+
+ void deleteByKey(String key);
+
+ void deleteByOwner(String owner);
+
+ void delete(AuthProfile authProfile);
+}
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
new file mode 100644
index 0000000..88613d9
--- /dev/null
+++
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
@@ -0,0 +1,38 @@
+/*
+ * 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.persistence.api.entity.auth;
+
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.core.persistence.api.entity.Entity;
+
+import java.util.List;
+
+public interface AuthProfile extends Entity {
+
+ String getOwner();
+
+ void setOwner(String owner);
+
+ List<GoogleMfaAuthToken> getGoogleMfaAuthTokens();
+
+ void setGoogleMfaAuthTokens(List<GoogleMfaAuthToken> tokens);
+
+ void add(GoogleMfaAuthToken token);
+}
diff --git
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAAuthProfileDAO.java
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAAuthProfileDAO.java
new file mode 100644
index 0000000..d688a85
--- /dev/null
+++
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAAuthProfileDAO.java
@@ -0,0 +1,91 @@
+/*
+ * 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.persistence.jpa.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.auth.AuthProfileDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+import org.apache.syncope.core.persistence.jpa.dao.AbstractDAO;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPAAuthProfile;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public class JPAAuthProfileDAO extends AbstractDAO<AuthProfile> implements
AuthProfileDAO {
+
+ @Override
+ @Transactional(readOnly = true)
+ public List<AuthProfile> findAll() {
+ TypedQuery<AuthProfile> query = entityManager().createQuery(
+ "SELECT e FROM " + JPAAuthProfile.class.getSimpleName() + " e ",
+ AuthProfile.class);
+ return query.getResultList();
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Optional<AuthProfile> findByOwner(final String owner) {
+ try {
+ TypedQuery<AuthProfile> query = entityManager().createQuery(
+ "SELECT e FROM " + JPAAuthProfile.class.getSimpleName()
+ + " e WHERE e.owner=:owner", AuthProfile.class);
+ query.setParameter("owner", owner);
+ return Optional.ofNullable(query.getSingleResult());
+ } catch (final NoResultException e) {
+ LOG.debug("No auth profile could be found for owner {}", owner);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Optional<AuthProfile> findByKey(final String key) {
+ try {
+ return
Optional.ofNullable(entityManager().find(JPAAuthProfile.class, key));
+ } catch (final NoResultException e) {
+ LOG.debug("No auth profile could be found for {}", key);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public AuthProfile save(final AuthProfile profile) {
+ return entityManager().merge(profile);
+ }
+
+ @Override
+ public void deleteByKey(final String key) {
+ findByKey(key).ifPresent(this::delete);
+ }
+
+ @Override
+ public void deleteByOwner(final String owner) {
+ findByOwner(owner).ifPresent(this::delete);
+ }
+
+ @Override
+ public void delete(final AuthProfile authProfile) {
+ entityManager().remove(authProfile);
+ }
+}
diff --git
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
index 7eafee6..131709d 100644
---
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
+++
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
@@ -58,6 +58,7 @@ import
org.apache.syncope.core.persistence.api.entity.anyobject.ARelationship;
import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
import org.apache.syncope.core.persistence.api.entity.auth.AuthModule;
import org.apache.syncope.core.persistence.api.entity.auth.AuthModuleItem;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
@@ -112,6 +113,7 @@ import
org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAPlainAttrUni
import
org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAPlainAttrValue;
import
org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAARelationship;
import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPAAuthProfile;
import org.apache.syncope.core.persistence.jpa.entity.auth.JPAOIDCRP;
import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SP;
import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SPKeystore;
@@ -333,6 +335,8 @@ public class JPAEntityFactory implements EntityFactory {
result = (E) new JPASAML2SPMetadata();
} else if (reference.equals(SAML2SPKeystore.class)) {
result = (E) new JPASAML2SPKeystore();
+ } else if (reference.equals(AuthProfile.class)) {
+ result = (E) new JPAAuthProfile();
} else {
throw new IllegalArgumentException("Could not find a JPA
implementation of " + reference.getName());
}
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
new file mode 100644
index 0000000..03e9586
--- /dev/null
+++
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
@@ -0,0 +1,81 @@
+/*
+ * 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.persistence.jpa.entity.auth;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+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;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Lob;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = JPAAuthProfile.TABLE, uniqueConstraints =
+ @UniqueConstraint(columnNames = { "owner" }))
+public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements
AuthProfile {
+
+ public static final String TABLE = "AuthProfile";
+
+ private static final long serialVersionUID = 57352617217394093L;
+
+ @Lob
+ private String googleMfaAuthTokens;
+
+ @Column(nullable = false)
+ private String owner;
+
+ @Override
+ public String getOwner() {
+ return owner;
+ }
+
+ @Override
+ public void setOwner(final String owner) {
+ this.owner = owner;
+ }
+
+ @Override
+ public List<GoogleMfaAuthToken> getGoogleMfaAuthTokens() {
+ return googleMfaAuthTokens == null
+ ? new ArrayList<>(0)
+ : POJOHelper.deserialize(googleMfaAuthTokens, new
TypeReference<List<GoogleMfaAuthToken>>() {
+ });
+ }
+
+ @Override
+ public void setGoogleMfaAuthTokens(final List<GoogleMfaAuthToken> tokens) {
+ this.googleMfaAuthTokens = POJOHelper.serialize(tokens);
+ }
+
+ @Override
+ public void add(final GoogleMfaAuthToken token) {
+ checkType(token, GoogleMfaAuthToken.class);
+ final List<GoogleMfaAuthToken> tokens = getGoogleMfaAuthTokens();
+ tokens.add(token);
+ setGoogleMfaAuthTokens(tokens);
+ }
+}
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
new file mode 100644
index 0000000..ec30f2c
--- /dev/null
+++
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.persistence.jpa.inner;
+
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+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;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Transactional("Master")
+public class AuthProfileTest extends AbstractTest {
+
+ @Autowired
+ private AuthProfileDAO authProfileDAO;
+
+ @Autowired
+ private EntityFactory entityFactory;
+
+ @Test
+ public void googleMfaToken() {
+ String id = UUID.randomUUID().toString();
+
+ createAuthProfileWithToken(id, 123456);
+
+ 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-New");
+ authProfile.getGoogleMfaAuthTokens().clear();
+ authProfileDAO.save(authProfile);
+
+ assertFalse(authProfileDAO.findByOwner(id).isPresent());
+ }
+
+ private AuthProfile createAuthProfileWithToken(final String owner, final
Integer otp) {
+ AuthProfile profile = entityFactory.newEntity(AuthProfile.class);
+ profile.setOwner(owner);
+ GoogleMfaAuthToken token = new GoogleMfaAuthToken.Builder()
+ .issueDate(new Date())
+ .token(otp)
+ .owner(owner)
+ .build();
+ profile.add(token);
+ return authProfileDAO.save(profile);
+ }
+}
diff --git
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AuthProfileDataBinder.java
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AuthProfileDataBinder.java
new file mode 100644
index 0000000..e102405
--- /dev/null
+++
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AuthProfileDataBinder.java
@@ -0,0 +1,29 @@
+/*
+ * 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.provisioning.api.data;
+
+import org.apache.syncope.common.lib.to.AuthProfileTO;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+
+public interface AuthProfileDataBinder {
+
+ AuthProfileTO getAuthProfileTO(AuthProfile authProfile);
+
+ AuthProfile create(AuthProfileTO authProfileTO);
+}
diff --git
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
new file mode 100644
index 0000000..0a78d86
--- /dev/null
+++
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
@@ -0,0 +1,50 @@
+/*
+ * 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.provisioning.java.data;
+
+import org.apache.syncope.common.lib.to.AuthProfileTO;
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AuthProfileDataBinderImpl implements AuthProfileDataBinder {
+ @Autowired
+ private EntityFactory entityFactory;
+
+ @Override
+ public AuthProfileTO getAuthProfileTO(final AuthProfile authProfile) {
+ AuthProfileTO authProfileTO = new AuthProfileTO();
+ authProfileTO.setKey(authProfile.getKey());
+ authProfileTO.setOwner(authProfile.getOwner());
+
authProfileTO.getGoogleMfaAuthTokens().addAll(authProfile.getGoogleMfaAuthTokens());
+ return authProfileTO;
+ }
+
+ @Override
+ public AuthProfile create(final AuthProfileTO authProfileTO) {
+ AuthProfile authProfile = entityFactory.newEntity(AuthProfile.class);
+ authProfile.setOwner(authProfileTO.getOwner());
+
authProfile.setGoogleMfaAuthTokens(authProfileTO.getGoogleMfaAuthTokens());
+ return authProfile;
+ }
+}
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 ca562eb..23c899a 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
@@ -105,6 +105,7 @@ import
org.apache.syncope.common.rest.api.service.AnyTypeClassService;
import org.apache.syncope.common.rest.api.service.AnyTypeService;
import org.apache.syncope.common.rest.api.service.ApplicationService;
import org.apache.syncope.common.rest.api.service.AuthModuleService;
+import org.apache.syncope.common.rest.api.service.AuthProfileService;
import org.apache.syncope.common.rest.api.service.CamelRouteService;
import org.apache.syncope.common.rest.api.service.ClientAppService;
import org.apache.syncope.common.rest.api.service.ConnectorService;
@@ -112,6 +113,7 @@ import
org.apache.syncope.common.rest.api.service.DynRealmService;
import org.apache.syncope.common.rest.api.service.LoggerService;
import org.apache.syncope.common.rest.api.service.NotificationService;
import org.apache.syncope.common.rest.api.service.SAML2SPKeystoreConfService;
+import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
import org.apache.syncope.common.rest.api.service.SAML2SPMetadataConfService;
import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
@@ -331,6 +333,10 @@ public abstract class AbstractITCase {
protected static ClientAppService clientAppService;
+ protected static GoogleMfaAuthTokenService googleMfaAuthTokenService;
+
+ protected static AuthProfileService authProfileService;
+
@BeforeAll
public static void securitySetup() {
try (InputStream propStream =
Encryptor.class.getResourceAsStream("/security.properties")) {
@@ -408,6 +414,8 @@ public abstract class AbstractITCase {
saml2IdPMetadataConfService =
adminClient.getService(SAML2IdPMetadataConfService.class);
saml2SPKeystoreService =
adminClient.getService(SAML2SPKeystoreService.class);
saml2SPKeystoreConfService =
adminClient.getService(SAML2SPKeystoreConfService.class);
+ googleMfaAuthTokenService =
adminClient.getService(GoogleMfaAuthTokenService.class);
+ authProfileService = adminClient.getService(AuthProfileService.class);
}
@Autowired
diff --git
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GoogleMfaAuthTokenITCase.java
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GoogleMfaAuthTokenITCase.java
new file mode 100644
index 0000000..fa20cac
--- /dev/null
+++
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GoogleMfaAuthTokenITCase.java
@@ -0,0 +1,147 @@
+/*
+ * 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.SyncopeClientException;
+import org.apache.syncope.common.lib.to.AuthProfileTO;
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import javax.ws.rs.core.Response;
+
+import java.security.SecureRandom;
+import java.time.LocalDateTime;
+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.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class GoogleMfaAuthTokenITCase extends AbstractITCase {
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private static GoogleMfaAuthToken createGoogleMfaAuthToken() {
+ Integer token = SECURE_RANDOM.ints(100_000, 999_999)
+ .findFirst()
+ .getAsInt();
+ return new GoogleMfaAuthToken.Builder()
+ .owner(UUID.randomUUID().toString())
+ .token(token)
+ .issueDate(new Date())
+ .build();
+ }
+
+ @BeforeEach
+ public void setup() {
+ googleMfaAuthTokenService.deleteTokens();
+ }
+
+ @Test
+ public void create() {
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ assertDoesNotThrow(new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ Response response = googleMfaAuthTokenService.save(token);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.CREATED.getStatusCode()) {
+ Exception ex =
clientFactory.getExceptionMapper().fromResponse(response);
+ if (ex != null) {
+ throw ex;
+ }
+ }
+ }
+ });
+ }
+
+ @Test
+ public void count() {
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ googleMfaAuthTokenService.save(token);
+ assertEquals(1, googleMfaAuthTokenService.countTokens());
+ assertEquals(1,
googleMfaAuthTokenService.countTokensForOwner(token.getOwner()));
+ }
+
+ @Test
+ public void verifyProfile() {
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ googleMfaAuthTokenService.save(token);
+ final List<AuthProfileTO> results = authProfileService.list();
+ assertFalse(results.isEmpty());
+ AuthProfileTO profileTO = results.get(0);
+ assertNotNull(authProfileService.findByKey(profileTO.getKey()));
+ assertNotNull(authProfileService.findByOwner(profileTO.getOwner()));
+ Response response = authProfileService.deleteByOwner(token.getOwner());
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(),
response.getStatus());
+ assertThrows(SyncopeClientException.class, () ->
authProfileService.findByOwner(token.getOwner()));
+ }
+
+ @Test
+ public void deleteByToken() {
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ Response response = googleMfaAuthTokenService.save(token);
+ String key = response.getHeaderString(RESTHeaders.RESOURCE_KEY);
+ assertNotNull(key);
+ response = googleMfaAuthTokenService.deleteToken(token.getToken());
+ assertEquals(response.getStatusInfo().getStatusCode(),
Response.Status.NO_CONTENT.getStatusCode());
+
assertTrue(googleMfaAuthTokenService.findTokensFor(token.getOwner()).isEmpty());
+ }
+
+ @Test
+ public void deleteByOwner() {
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ Response response = googleMfaAuthTokenService.save(token);
+ String key = response.getHeaderString(RESTHeaders.RESOURCE_KEY);
+ assertNotNull(key);
+ response = googleMfaAuthTokenService.deleteTokensFor(token.getOwner());
+ assertEquals(response.getStatusInfo().getStatusCode(),
Response.Status.NO_CONTENT.getStatusCode());
+
assertTrue(googleMfaAuthTokenService.findTokensFor(token.getOwner()).isEmpty());
+ }
+
+ @Test
+ public void deleteByOwnerAndToken() {
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ Response response = googleMfaAuthTokenService.save(token);
+ String key = response.getHeaderString(RESTHeaders.RESOURCE_KEY);
+ assertNotNull(key);
+ response = googleMfaAuthTokenService.deleteToken(token.getOwner(),
token.getToken());
+ assertEquals(response.getStatusInfo().getStatusCode(),
Response.Status.NO_CONTENT.getStatusCode());
+
assertTrue(googleMfaAuthTokenService.findTokensFor(token.getOwner()).isEmpty());
+ }
+
+ @Test
+ public void deleteByDate() {
+ Date dateTime =
Date.from(LocalDateTime.now().minusDays(1).atZone(ZoneId.systemDefault()).toInstant());
+ GoogleMfaAuthToken token = createGoogleMfaAuthToken();
+ final Response response =
googleMfaAuthTokenService.deleteTokensByDate(dateTime);
+ assertEquals(response.getStatusInfo().getStatusCode(),
Response.Status.NO_CONTENT.getStatusCode());
+
assertTrue(googleMfaAuthTokenService.findTokensFor(token.getOwner()).isEmpty());
+ assertEquals(0,
googleMfaAuthTokenService.countTokensForOwner(token.getOwner()));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 78d9946..fd26150 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1496,6 +1496,11 @@ under the License.
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-core-authentication-api</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-services-registry</artifactId>
<version>${cas.version}</version>
</dependency>
@@ -1651,6 +1656,16 @@ under the License.
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-gauth-core-mfa</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-otp-mfa-core</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-oidc-services</artifactId>
<version>${cas.version}</version>
</dependency>
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 10c3cba..16e7126 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -116,6 +116,10 @@ under the License.
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-core-authentication-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-webflow</artifactId>
</dependency>
<dependency>
@@ -212,6 +216,14 @@ under the License.
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-gauth-core-mfa</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-otp-mfa-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp-config</artifactId>
</dependency>
<dependency>
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
index 2a85826..b5c6994 100644
---
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
@@ -20,6 +20,7 @@ package org.apache.syncope.wa.starter;
import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
import org.apereo.cas.configuration.CasConfigurationProperties;
+import org.apereo.cas.otp.repository.token.OneTimeTokenRepository;
import org.apereo.cas.services.ServiceRegistryExecutionPlanConfigurer;
import org.apereo.cas.services.ServiceRegistryListener;
import
org.apereo.cas.support.pac4j.authentication.DelegatedClientFactoryCustomizer;
@@ -33,6 +34,7 @@ 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;
import org.apache.syncope.wa.bootstrap.WARestClient;
+import
org.apache.syncope.wa.starter.gauth.token.SyncopeWAGoogleMfaAuthTokenRepository;
import org.apache.syncope.wa.starter.mapping.AccessMapFor;
import org.apache.syncope.wa.starter.mapping.AccessMapper;
import org.apache.syncope.wa.starter.mapping.AttrReleaseMapFor;
@@ -168,6 +170,13 @@ public class SyncopeWAConfiguration {
}
@Bean
+ @Autowired
+ public OneTimeTokenRepository
oneTimeTokenAuthenticatorTokenRepository(final WARestClient restClient) {
+ return new SyncopeWAGoogleMfaAuthTokenRepository(restClient,
+ casProperties.getAuthn().getMfa().getGauth().getTimeStepSize());
+ }
+
+ @Bean
public KeymasterStart keymasterStart() {
return new KeymasterStart(NetworkService.Type.WA);
}
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/token/SyncopeWAGoogleMfaAuthTokenRepository.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/token/SyncopeWAGoogleMfaAuthTokenRepository.java
new file mode 100644
index 0000000..efd6be1
--- /dev/null
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/token/SyncopeWAGoogleMfaAuthTokenRepository.java
@@ -0,0 +1,143 @@
+/*
+ * 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.gauth.token;
+
+import org.apereo.cas.authentication.OneTimeToken;
+import org.apereo.cas.gauth.token.GoogleAuthenticatorToken;
+import org.apereo.cas.otp.repository.token.BaseOneTimeTokenRepository;
+
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
+import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Date;
+
+public class SyncopeWAGoogleMfaAuthTokenRepository extends
BaseOneTimeTokenRepository {
+ private static final Logger LOG =
LoggerFactory.getLogger(SyncopeWAGoogleMfaAuthTokenRepository.class);
+
+ private final WARestClient waRestClient;
+
+ private final long expireTokensInSeconds;
+
+ public SyncopeWAGoogleMfaAuthTokenRepository(final WARestClient
waRestClient,
+ final long
expireTokensInSeconds) {
+ this.waRestClient = waRestClient;
+ this.expireTokensInSeconds = expireTokensInSeconds;
+ }
+
+ @Override
+ protected void cleanInternal() {
+ Date expirationDate = Date.from(LocalDateTime.
+ now(ZoneOffset.UTC).
+ minusSeconds(this.expireTokensInSeconds).
+ toInstant(ZoneOffset.UTC));
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ tokenService.deleteTokensByDate(expirationDate);
+ }
+
+ @Override
+ public void store(final OneTimeToken token) {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ GoogleMfaAuthToken tokenTO = new GoogleMfaAuthToken.Builder()
+ .owner(token.getUserId())
+ .token(token.getToken())
+
.issueDate(Date.from(token.getIssuedDateTime().toInstant(ZoneOffset.UTC)))
+ .build();
+ tokenService.save(tokenTO);
+ }
+
+ @Override
+ public OneTimeToken get(final String username, final Integer otp) {
+ try {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ GoogleMfaAuthToken tokenTO = tokenService.findTokenFor(username,
otp);
+ GoogleAuthenticatorToken token = new
GoogleAuthenticatorToken(tokenTO.getToken(), tokenTO.getOwner());
+ LocalDateTime dateTime =
tokenTO.getIssueDate().toInstant().atZone(ZoneOffset.UTC).toLocalDateTime();
+ token.setIssuedDateTime(dateTime);
+ return token;
+ } catch (final Exception e) {
+ LOG.debug("Unable to fetch token {} for user {}", otp, username);
+ }
+ return null;
+ }
+
+ @Override
+ public void remove(final String username, final Integer otp) {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ Response response = tokenService.deleteToken(username, otp);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.NO_CONTENT.getStatusCode()) {
+ throw new RuntimeException("Unable to remove token " + otp + " for
user " + username);
+ }
+ }
+
+ @Override
+ public void remove(final String username) {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ Response response = tokenService.deleteTokensFor(username);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.NO_CONTENT.getStatusCode()) {
+ throw new RuntimeException("Unable to remove tokens for user " +
username);
+ }
+ }
+
+ @Override
+ public void remove(final Integer otp) {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ Response response = tokenService.deleteToken(otp);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.NO_CONTENT.getStatusCode()) {
+ throw new RuntimeException("Unable to remove token " + otp);
+ }
+ }
+
+ @Override
+ public void removeAll() {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ Response response = tokenService.deleteTokens();
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.NO_CONTENT.getStatusCode()) {
+ throw new RuntimeException("Unable to remove tokens");
+ }
+ }
+
+ @Override
+ public long count(final String username) {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ return tokenService.countTokensForOwner(username);
+ }
+
+ @Override
+ public long count() {
+ GoogleMfaAuthTokenService tokenService =
waRestClient.getSyncopeClient().
+ getService(GoogleMfaAuthTokenService.class);
+ return tokenService.countTokens();
+ }
+}
diff --git
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
index 16d4cab..de35ba5 100644
---
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
+++
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
@@ -20,15 +20,24 @@ package org.apache.syncope.wa.starter;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import javax.validation.constraints.NotNull;
import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.syncope.common.keymaster.client.api.ServiceOps;
import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.lib.types.GoogleMfaAuthToken;
import org.apache.syncope.common.lib.wa.WAClientApp;
+import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
import org.apache.syncope.common.rest.api.service.wa.WAClientAppService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
@@ -52,10 +61,13 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
// 1. start (mocked) Core as embedded CXF
JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
sf.setAddress(ADDRESS);
- sf.setResourceClasses(WAClientAppService.class);
+ sf.setResourceClasses(WAClientAppService.class,
GoogleMfaAuthTokenService.class);
sf.setResourceProvider(
WAClientAppService.class,
new SingletonResourceProvider(new
StubWAClientAppService(), true));
+ sf.setResourceProvider(
+ GoogleMfaAuthTokenService.class,
+ new SingletonResourceProvider(new
StubGoogleMfaAuthTokenService(), true));
sf.setProviders(List.of(new JacksonJsonProvider()));
sf.create();
@@ -68,7 +80,81 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
}
}
- public class StubWAClientAppService implements WAClientAppService {
+ public static class StubGoogleMfaAuthTokenService implements
GoogleMfaAuthTokenService {
+ private final List<GoogleMfaAuthToken> tokens = new ArrayList<>();
+
+ @Override
+ public Response deleteTokensByDate(@NotNull final Date expirationDate)
{
+ tokens.removeIf(token ->
token.getIssueDate().compareTo(expirationDate) >= 0);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteToken(@NotNull final String owner, @NotNull
final Integer token) {
+ tokens.removeIf(to -> to.getToken().equals(token) &&
to.getOwner().equalsIgnoreCase(owner));
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteTokensFor(@NotNull final String owner) {
+ tokens.removeIf(to -> to.getOwner().equalsIgnoreCase(owner));
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteToken(@NotNull final Integer token) {
+ tokens.removeIf(to -> to.getToken().equals(token));
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response deleteTokens() {
+ tokens.clear();
+ return Response.noContent().build();
+ }
+
+ @Override
+ public Response save(@NotNull final GoogleMfaAuthToken tokenTO) {
+ tokenTO.setKey(UUID.randomUUID().toString());
+ tokens.add(tokenTO);
+ return Response.ok().build();
+ }
+
+ @Override
+ public GoogleMfaAuthToken findTokenFor(@NotNull final String owner,
@NotNull final Integer token) {
+ return tokens.stream()
+ .filter(to -> to.getToken().equals(token) &&
to.getOwner().equalsIgnoreCase(owner))
+ .findFirst().get();
+ }
+
+ @Override
+ public List<GoogleMfaAuthToken> findTokensFor(@NotNull final String
user) {
+ return tokens.stream()
+ .filter(to -> to.getOwner().equalsIgnoreCase(user))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public GoogleMfaAuthToken findTokenFor(@NotNull final String key) {
+ return tokens.stream()
+ .filter(to -> to.getKey().equalsIgnoreCase(key))
+ .findFirst().get();
+ }
+
+ @Override
+ public long countTokensForOwner(@NotNull final String user) {
+ return tokens.stream()
+ .filter(to -> to.getOwner().equalsIgnoreCase(user))
+ .count();
+ }
+
+ @Override
+ public long countTokens() {
+ return tokens.size();
+ }
+ }
+
+ public static class StubWAClientAppService implements WAClientAppService {
@Override
public List<WAClientApp> list() {
@@ -78,13 +164,13 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
@Override
public WAClientApp read(final Long clientAppId, final ClientAppType
type) {
return APPS.stream().filter(app -> Objects.equals(clientAppId,
app.getClientAppTO().getClientAppId())).
- findFirst().orElseThrow(() -> new
NotFoundException("ClientApp with clientId " + clientAppId));
+ findFirst().orElseThrow(() -> new NotFoundException("ClientApp
with clientId " + clientAppId));
}
@Override
public WAClientApp read(final String name, final ClientAppType type) {
return APPS.stream().filter(app -> Objects.equals(name,
app.getClientAppTO().getName())).
- findFirst().orElseThrow(() -> new
NotFoundException("ClientApp with name " + name));
+ findFirst().orElseThrow(() -> new NotFoundException("ClientApp
with name " + name));
}
}
}
diff --git
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/SyncopeWAGoogleMfaAuthTokenRepositoryTest.java
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/SyncopeWAGoogleMfaAuthTokenRepositoryTest.java
new file mode 100644
index 0000000..0edb6fc
--- /dev/null
+++
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/SyncopeWAGoogleMfaAuthTokenRepositoryTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.gauth.token;
+
+import org.apereo.cas.gauth.token.GoogleAuthenticatorToken;
+import org.apereo.cas.otp.repository.token.OneTimeTokenRepository;
+
+import org.apache.syncope.wa.starter.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SyncopeWAGoogleMfaAuthTokenRepositoryTest extends AbstractTest {
+ @Autowired
+ private OneTimeTokenRepository tokenRepository;
+
+ @Test
+ public void verifyOps() {
+ tokenRepository.removeAll();
+ GoogleAuthenticatorToken token = new GoogleAuthenticatorToken(123456,
"SyncopeWA");
+ tokenRepository.store(token);
+ assertEquals(1, tokenRepository.count(token.getUserId()));
+ assertEquals(1, tokenRepository.count());
+ }
+}