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());
+    }
+}

Reply via email to